Compare commits
10 Commits
3d6bc97411
...
8a626b17fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a626b17fc | ||
|
|
a5886b7c92 | ||
|
|
2ef2c0edbf | ||
|
|
4b2ca7ce0e | ||
|
|
8a8aeaedd7 | ||
|
|
28e62093b3 | ||
|
|
64d1772842 | ||
|
|
10b1d53f9d | ||
|
|
88ff9e08a9 | ||
|
|
2e961252f7 |
@@ -217,13 +217,13 @@ Returns the change history of an article by its SKU.
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>` — The article history entries.
|
||||
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` — The article history entries.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||
// => [{ id, companyuserId, date, text }]
|
||||
// => [{ id, companyuserId, date, text, meta }]
|
||||
|
||||
const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
||||
// => up to 5 most recent entries
|
||||
|
||||
19
API.articlehistory.md
Normal file
19
API.articlehistory.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# articlehistory
|
||||
|
||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||
|
||||
## `articlehistory.record`
|
||||
|
||||
*Source: `src/store/articlehistory/record.js`*
|
||||
|
||||
Formats a numeric currency amount in German language style: "€ 1.000,99"
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `amount` | `number|string` | The currency amount. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`string` — The formatted currency string.
|
||||
@@ -171,7 +171,7 @@ const articles = await store.category.getCategoryArticles(42, 5);
|
||||
|
||||
*Source: `src/store/category/getTreeForCompany.js`*
|
||||
|
||||
Returns the full category tree for the user's selected company together with its articles and the posterminal ids assigned to each category.
|
||||
Returns the full category tree for the user's selected company together with the article count and the posterminal ids assigned to each category.
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -182,13 +182,13 @@ Returns the full category tree for the user's selected company together with its
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>` — The category tree, articles and language.
|
||||
`Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>` — The category tree and article count.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const tree = await store.category.getTreeForCompany(42);
|
||||
// => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
||||
// => { languageId, tree: [{ id, articleCount, posterminalIds, ... }], articleCount: 25 }
|
||||
```
|
||||
|
||||
## `category.move`
|
||||
|
||||
@@ -186,13 +186,13 @@ Lists the companies accessible to the given Google auth user.
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<Array<object>>` — The accessible company rows.
|
||||
`Promise<Array<{id: number, name: string, rootCategoryId: number, languageId: number, iso639: string|null, taxrate: number, reducedtaxrate: number, taxfreerate: number}>>` — The accessible company rows.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const companies = await store.company.list(42);
|
||||
// => [{ id, name, ... }]
|
||||
// => [{ id, name, rootCategoryId, languageId, iso639, taxrate, reducedtaxrate, taxfreerate }]
|
||||
```
|
||||
|
||||
## `company.update`
|
||||
|
||||
210
API.mcp.md
Normal file
210
API.mcp.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# mcp
|
||||
|
||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||
|
||||
## `mcp.consumeAuthCode`
|
||||
|
||||
*Source: `src/store/mcp/consumeAuthCode.js`*
|
||||
|
||||
Marks an authorization code as used.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `code` | `string` | The authorization code to mark used. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<boolean>` — True once marked.
|
||||
|
||||
## `mcp.createAuthCode`
|
||||
|
||||
*Source: `src/store/mcp/createAuthCode.js`*
|
||||
|
||||
Stores an OAuth 2.1 authorization code with PKCE parameters.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `input` | `object` | |
|
||||
| `input.code` | `string` | The generated authorization code. |
|
||||
| `input.clientId` | `string` | The client ID. |
|
||||
| `input.googleAuthId` | `number` | The ID of the authenticated user. |
|
||||
| `input.redirectUri` | `string` | The redirect URI used in the authorize request. |
|
||||
| `input.codeChallenge` | `string` | The PKCE code challenge. |
|
||||
| `input.codeChallengeMethod` | `string` | The PKCE method (e.g. S256). |
|
||||
| `[input.scope]` | `string|null` | Granted scope. |
|
||||
| `input.expiresAt` | `string|Date` | Expiration ISO string or Date. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<string>` — The authorization code.
|
||||
|
||||
## `mcp.createRefreshToken`
|
||||
|
||||
*Source: `src/store/mcp/createRefreshToken.js`*
|
||||
|
||||
Stores an OAuth 2.1 refresh token.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `input` | `object` | |
|
||||
| `input.refreshToken` | `string` | The refresh token string. |
|
||||
| `input.clientId` | `string` | The client ID. |
|
||||
| `input.googleAuthId` | `number` | The user ID. |
|
||||
| `[input.scope]` | `string|null` | Scopes granted. |
|
||||
| `input.expiresAt` | `string|Date` | Expiration Date or ISO string. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<object>` — Created token record or id.
|
||||
|
||||
## `mcp.deleteClient`
|
||||
|
||||
*Source: `src/store/mcp/deleteClient.js`*
|
||||
|
||||
Deletes a registered MCP client by client ID.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `clientId` | `string` | The client identifier to delete. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<boolean>` — True if deleted.
|
||||
|
||||
## `mcp.getAuthCode`
|
||||
|
||||
*Source: `src/store/mcp/getAuthCode.js`*
|
||||
|
||||
Retrieves an authorization code record.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `code` | `string` | The authorization code. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<object|null>` — The auth code record or null.
|
||||
|
||||
## `mcp.getClient`
|
||||
|
||||
*Source: `src/store/mcp/getClient.js`*
|
||||
|
||||
Retrieves an MCP client by client ID.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `clientId` | `string` | The client identifier. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<object|null>` — The client record or null if not found.
|
||||
|
||||
## `mcp.getRefreshToken`
|
||||
|
||||
*Source: `src/store/mcp/getRefreshToken.js`*
|
||||
|
||||
Retrieves an OAuth 2.1 refresh token.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `refreshToken` | `string` | The refresh token. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<object|null>` — The refresh token record or null.
|
||||
|
||||
## `mcp.listAuthorizations`
|
||||
|
||||
*Source: `src/store/mcp/listAuthorizations.js`*
|
||||
|
||||
Lists active authorizations for a Google Auth user.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `googleAuthId` | `number` | The user ID. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<Array<object>>` — List of authorized clients and token counts.
|
||||
|
||||
## `mcp.listClients`
|
||||
|
||||
*Source: `src/store/mcp/listClients.js`*
|
||||
|
||||
Lists all registered MCP clients.
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<Array<object>>` — List of client records.
|
||||
|
||||
## `mcp.registerClient`
|
||||
|
||||
*Source: `src/store/mcp/registerClient.js`*
|
||||
|
||||
Registers a new OAuth MCP client.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `input` | `object` | |
|
||||
| `input.clientId` | `string` | Unique client identifier. |
|
||||
| `[input.clientSecret]` | `string|null` | Optional client secret for confidential clients. |
|
||||
| `input.clientName` | `string` | Human-readable client name. |
|
||||
| `input.redirectUris` | `string|Array<string>` | JSON string or array of redirect URIs. |
|
||||
| `[input.grantTypes]` | `string|Array<string>` | Supported grant types. |
|
||||
| `[input.responseTypes]` | `string|Array<string>` | Supported response types. |
|
||||
| `[input.scope]` | `string|null` | Allowed scope. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<object>` — The created client record.
|
||||
|
||||
## `mcp.revokeAuthorizations`
|
||||
|
||||
*Source: `src/store/mcp/revokeAuthorizations.js`*
|
||||
|
||||
Revokes all active authorizations (tokens) for a user and MCP client.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `googleAuthId` | `number` | The user ID. |
|
||||
| `clientId` | `string` | The MCP client ID. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<boolean>` — True once revoked.
|
||||
|
||||
## `mcp.revokeRefreshToken`
|
||||
|
||||
*Source: `src/store/mcp/revokeRefreshToken.js`*
|
||||
|
||||
Revokes an OAuth 2.1 refresh token.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `refreshToken` | `string` | The refresh token to revoke. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<boolean>` — True once revoked.
|
||||
50
API.md
50
API.md
@@ -11,7 +11,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)`
|
||||
- **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)`
|
||||
- **articleean**: `id PK, article_id->article, ean:s(50) UQ`
|
||||
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt`
|
||||
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt, meta?:txt`
|
||||
- **articlelist**: `id PK`
|
||||
- **articlelistarticle**: `id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)`
|
||||
- **articlepic**: `id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)`
|
||||
@@ -72,7 +72,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `getById(id: number, tx?: object): Promise<object|undefined>`
|
||||
- `getByName(name: string, tx?: object): Promise<object|undefined>`
|
||||
- `getHistory(googleAuthId: number, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, rootCategoryId: number, languageId: number, iso639: string|null, taxrate: number, reducedtaxrate: number, taxfreerate: number}>>`
|
||||
- `update(id: number, input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>`
|
||||
- [category](API.category.md)
|
||||
- `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>`
|
||||
@@ -81,7 +81,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>`
|
||||
- `ensureByPath(input: {googleAuthId: number, parentId: number, path: string}, tx?: object): Promise<number>`
|
||||
- `getCategoryArticles(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>`
|
||||
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>`
|
||||
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>`
|
||||
- `move(input: {googleAuthId: number, id: number, parentId: number}, tx?: object): Promise<{id: number, parentId: number, sort: number}>`
|
||||
- `order(input: {googleAuthId: number, parentId: number, categoryIds: Array<number>}, tx?: object): Promise<{categoryIds: Array<number>}>`
|
||||
- `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>`
|
||||
@@ -93,7 +93,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
|
||||
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
|
||||
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>`
|
||||
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
||||
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>`
|
||||
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
|
||||
- `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
|
||||
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
|
||||
@@ -184,8 +184,20 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, offset: number, limit: number}|Array<object>>`
|
||||
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, offset: number, limit: number}|Array<object>>`
|
||||
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- `syncSummary(): void`
|
||||
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||
- [shipment](API.shipment.md)
|
||||
- `create(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- `delete(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- `getById(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- `listByOrderId(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- [shipping](API.shipping.md)
|
||||
- `create(googleAuthId: number, input: {manifestId: number, type: string, state: string, trackingCode?: string, carrier?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>`
|
||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
|
||||
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
- [manifestattachment](API.manifestattachment.md)
|
||||
- `getById(googleAuthId: number, manifestId: number, id: number, tx?: object): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>`
|
||||
- `listByManifestId(): void`
|
||||
@@ -285,6 +297,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>`
|
||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, type: string, name: string}>>`
|
||||
- [articlehistory](API.articlehistory.md)
|
||||
- `record(amount: number|string): string`
|
||||
- [articlelist](API.articlelist.md)
|
||||
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||
@@ -295,12 +309,25 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `delete(googleAuthId: number, categoryId: number, propertyId: number, tx?: object): Promise<void>`
|
||||
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||
- `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||
- [mcp](API.mcp.md)
|
||||
- `consumeAuthCode(code: string): Promise<boolean>`
|
||||
- `createAuthCode(input: {code: string, clientId: string, googleAuthId: number, redirectUri: string, codeChallenge: string, codeChallengeMethod: string, scope?: string|null, expiresAt: string|Date}): Promise<string>`
|
||||
- `createRefreshToken(input: {refreshToken: string, clientId: string, googleAuthId: number, scope?: string|null, expiresAt: string|Date}): Promise<object>`
|
||||
- `deleteClient(clientId: string): Promise<boolean>`
|
||||
- `getAuthCode(code: string): Promise<object|null>`
|
||||
- `getClient(clientId: string): Promise<object|null>`
|
||||
- `getRefreshToken(refreshToken: string): Promise<object|null>`
|
||||
- `listAuthorizations(googleAuthId: number): Promise<Array<object>>`
|
||||
- `listClients(): Promise<Array<object>>`
|
||||
- `registerClient(input: {clientId: string, clientSecret?: string|null, clientName: string, redirectUris: string|Array<string>, grantTypes?: string|Array<string>, responseTypes?: string|Array<string>, scope?: string|null}): Promise<object>`
|
||||
- `revokeAuthorizations(googleAuthId: number, clientId: string): Promise<boolean>`
|
||||
- `revokeRefreshToken(refreshToken: string): Promise<boolean>`
|
||||
- [modelconversation](API.modelconversation.md)
|
||||
- `create(googleAuthId: number, input: {provider: string, model: string}, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
- `create(googleAuthId: number, input: {provider: string, model: string, started_by?: number|null}, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||
- `delete(googleAuthId: number, conversationId: number, tx?: object): Promise<void>`
|
||||
- `get(googleAuthId: number, conversationId: number, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>`
|
||||
- `setState(googleAuthId: number, conversationId: number, state: string, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
- `get(googleAuthId: number, conversationId: number, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>>`
|
||||
- `setState(googleAuthId: number, conversationId: number, state: string, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||
- [modelmessage](API.modelmessage.md)
|
||||
- `add(googleAuthId: number, conversationId: number, input: {message: string, role?: 'user'|'assistant'|'system'|'tool'|'reasoning'|'usage'}, tx?: object): Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>`
|
||||
- `list(googleAuthId: number, conversationId: number, tx?: object): Promise<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>`
|
||||
@@ -323,8 +350,3 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
|
||||
- [runner.js](API.runner.js.md)
|
||||
- `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`
|
||||
- [shipping](API.shipping.md)
|
||||
- `create(googleAuthId: number, input: {manifestId: number, type: string, state: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, type: string, state: string}>`
|
||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
|
||||
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||
|
||||
@@ -16,15 +16,16 @@ Creates a new (idle) model conversation for the user in the selected company.
|
||||
| `input` | `object` | Conversation input. |
|
||||
| `input.provider` | `string` | LLM provider name. |
|
||||
| `input.model` | `string` | Model identifier. |
|
||||
| `[input.started_by]` | `number|null` | Id of the conversation that starts this one as a subagent (unenforced reference; omit/null for a top-level conversation). |
|
||||
| `[tx]` | `object` | Optional transaction to run within. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||
|
||||
**Throws**
|
||||
|
||||
`Error` — 'Provider required' or 'Model required'.
|
||||
`Error` — 'Provider required', 'Model required' or 'Invalid started_by'.
|
||||
|
||||
**Example**
|
||||
|
||||
@@ -36,7 +37,7 @@ const c = await store.modelconversation.create(42, { provider: 'openrouter', mod
|
||||
|
||||
*Source: `src/store/modelconversation/delete.js`*
|
||||
|
||||
Deletes a model conversation by ID for the user in the selected company.
|
||||
Deletes a model conversation by ID for the user in the selected company, together with every subagent conversation below it (`started_by` chain, recursively) and their messages.
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -70,7 +71,7 @@ Gets a single model conversation by ID for the user in the selected company.
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>` — `first_user_message` is the stored `message` payload of the first `user` message (null if none yet).
|
||||
|
||||
**Throws**
|
||||
|
||||
@@ -91,7 +92,7 @@ Lists the user's own model conversations in the selected company, newest activit
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>`
|
||||
`Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>>` — `started_by` is the originating conversation id for subagent conversations (null otherwise), so clients can build a tree. `first_user_message` is the stored `message` payload of the conversation's first `user` message (null if none yet).
|
||||
|
||||
**Example**
|
||||
|
||||
@@ -116,7 +117,7 @@ Sets the state of a model conversation ('idle' | 'busy').
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>`
|
||||
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||
|
||||
**Throws**
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
||||
|
||||
*Source: `src/store/numbers/list.js`*
|
||||
|
||||
Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery') exist.
|
||||
Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment') exist.
|
||||
|
||||
**Parameters**
|
||||
|
||||
|
||||
32
API.order.md
32
API.order.md
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
||||
|
||||
*Source: `src/store/order/create.js`*
|
||||
|
||||
Creates a new order for the user's selected company with its lines.
|
||||
Creates a new order for the user's selected company with its lines and records article history.
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -104,9 +104,13 @@ Lists the orders of the user's selected company.
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||
| `[opts]` | `object` | Optional pagination options. |
|
||||
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||
| `[opts.offset=0]` | `number` | Number of orders to skip. |
|
||||
| `[opts.limit=20]` | `number` | Number of orders to return. |
|
||||
| `[opts.limit=50]` | `number` | Number of orders to return. |
|
||||
| `[opts.search]` | `string` | Free-text search matching order number, external number, or customer name. |
|
||||
| `[opts.onlyUnpaid=false]` | `boolean` | When true, returns only unpaid orders. |
|
||||
| `[opts.onlyUndelivered=false]` | `boolean` | When true, returns only undelivered orders. |
|
||||
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||
| `[tx]` | `object` | Optional transaction. |
|
||||
|
||||
**Returns**
|
||||
@@ -116,13 +120,31 @@ Lists the orders of the user's selected company.
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const result = await store.order.list(42, { offset: 0, limit: 20 });
|
||||
// => { orders: [...], total: 100, offset: 0, limit: 20 }
|
||||
const result = await store.order.list(42, { offset: 0, limit: 50, search: '1002', onlyUnpaid: true, onlyUndelivered: true, seekMonths: -1 });
|
||||
// => { orders: [...], total: 10, offset: 0, limit: 50 }
|
||||
|
||||
const orders = await store.order.list(42);
|
||||
// => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }]
|
||||
```
|
||||
|
||||
## `order.rebuildSummaries`
|
||||
|
||||
*Source: `src/store/order/rebuildSummaries.js`*
|
||||
|
||||
Rebuilds the `ordersummary` table for the user's selected company.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||
|
||||
## `order.syncSummary`
|
||||
|
||||
*Source: `src/store/order/syncSummary.js`*
|
||||
|
||||
Centralized sync and rebuild logic for the materialized `ordersummary` table.
|
||||
|
||||
## `order.update`
|
||||
|
||||
*Source: `src/store/order/update.js`*
|
||||
|
||||
51
API.shipment.md
Normal file
51
API.shipment.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# shipment
|
||||
|
||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||
|
||||
## `shipment.create`
|
||||
|
||||
*Source: `src/store/shipment/create.js`*
|
||||
|
||||
Creates a new shipment manifest for an order. Validates that all article positions belong to the order and do not exceed remaining ordered quantities.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||
|
||||
## `shipment.delete`
|
||||
|
||||
*Source: `src/store/shipment/delete.js`*
|
||||
|
||||
Deletes a shipment manifest of the selected company. Shipped shipments (with packages in 'shipped' state) cannot be deleted.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||
|
||||
## `shipment.getById`
|
||||
|
||||
*Source: `src/store/shipment/getById.js`*
|
||||
|
||||
Retrieves a shipment manifest by ID for the selected company, including lines and packages.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||
|
||||
## `shipment.listByOrderId`
|
||||
|
||||
*Source: `src/store/shipment/listByOrderId.js`*
|
||||
|
||||
Lists all shipment manifests belonging to an order.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
||||
|
||||
*Source: `src/store/shipping/create.js`*
|
||||
|
||||
Creates a shipping for a manifest (order). The number is allocated from the `shipping` numbers sequence (own prefix + counter). If state is 'shipped', adjusts stock by appending negative ledger movements for tracked order lines, writes article history, and ensures the order was not already shipped.
|
||||
Creates a shipping (package) for a shipment manifest. The number is allocated from the `shipping` numbers sequence (own prefix + counter). If state is 'shipped', adjusts stock by appending negative ledger movements for tracked shipment lines, writes article history for all items, and ensures stock is deducted once.
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -14,24 +14,26 @@ Creates a shipping for a manifest (order). The number is allocated from the `shi
|
||||
| --- | --- | --- |
|
||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||
| `input` | `object` | Shipping input. |
|
||||
| `input.manifestId` | `number` | The manifest (order) id. |
|
||||
| `input.manifestId` | `number` | The shipment manifest id. |
|
||||
| `input.type` | `string` | Shipping type. |
|
||||
| `input.state` | `string` | Shipping state ('shipped' or other). |
|
||||
| `[input.trackingCode]` | `string` | Carrier tracking code. |
|
||||
| `[input.carrier]` | `string` | Carrier name. |
|
||||
| `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). |
|
||||
| `[tx]` | `object` | Optional transaction to run within. |
|
||||
|
||||
**Returns**
|
||||
|
||||
`Promise<{id: number, manifestId: number, number: string, type: string, state: string}>`
|
||||
`Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>`
|
||||
|
||||
**Throws**
|
||||
|
||||
`Error` — If manifest is not found, order already shipped, or input is invalid.
|
||||
`Error` — If manifest is not found, manifest is not a shipment, or input is invalid.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
await store.shipping.create(googleAuthId, { manifestId: 42, type: 'parcel', state: 'shipped' });
|
||||
await store.shipping.create(googleAuthId, { manifestId: 42, type: 'parcel', state: 'shipped', trackingCode: '1Z999', carrier: 'UPS' });
|
||||
```
|
||||
|
||||
## `shipping.delete`
|
||||
|
||||
@@ -27,6 +27,8 @@ const MODULE_ORDER = [
|
||||
'delivery',
|
||||
'quote',
|
||||
'order',
|
||||
'shipment',
|
||||
'shipping',
|
||||
'manifestattachment',
|
||||
'ledger',
|
||||
'warehouse',
|
||||
@@ -322,7 +324,7 @@ const SCHEMA_TABLES = [
|
||||
['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'],
|
||||
['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'],
|
||||
['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
|
||||
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'],
|
||||
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt, meta?:txt'],
|
||||
['articlelist', 'id PK'],
|
||||
['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'],
|
||||
['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1246,49 +1246,53 @@
|
||||
<!-- articlehistory -->
|
||||
<g id="node23" class="node">
|
||||
<title>articlehistory</title>
|
||||
<polygon fill="#33415c" stroke="none" points="1648.85,-1073.6 1648.85,-1098 1814.89,-1098 1814.89,-1073.6 1648.85,-1073.6"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1073.6 1648.85,-1098 1814.89,-1098 1814.89,-1073.6 1648.85,-1073.6"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1694.19" y="-1083.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">articlehistory</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1046.8 1648.85,-1073.6 1814.89,-1073.6 1814.89,-1046.8 1648.85,-1046.8"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1046.8 1648.85,-1073.6 1814.89,-1073.6 1814.89,-1046.8 1648.85,-1046.8"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1057" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.39" y="-1058.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.39" y="-1058.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1020 1648.85,-1046.8 1814.89,-1046.8 1814.89,-1020 1648.85,-1020"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1020 1648.85,-1046.8 1814.89,-1046.8 1814.89,-1020 1648.85,-1020"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1030.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">article_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1031.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1031.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-993.2 1648.85,-1020 1814.89,-1020 1814.89,-993.2 1648.85,-993.2"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-993.2 1648.85,-1020 1814.89,-1020 1814.89,-993.2 1648.85,-993.2"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1003.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">companyuser_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1765.11" y="-1003.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1004.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1004.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
|
||||
<polygon fill="#272e3d" stroke="none" points="1648.85,-966.4 1648.85,-993.2 1814.89,-993.2 1814.89,-966.4 1648.85,-966.4"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-966.4 1648.85,-993.2 1814.89,-993.2 1814.89,-966.4 1648.85,-966.4"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-974.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">date</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1759.39" y="-976.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
|
||||
<polygon fill="#2a3242" stroke="none" points="1648.85,-939.6 1648.85,-966.4 1814.89,-966.4 1814.89,-939.6 1648.85,-939.6"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-939.6 1648.85,-966.4 1814.89,-966.4 1814.89,-939.6 1648.85,-939.6"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-947.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">text</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-949.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
|
||||
<polygon fill="#33415c" stroke="none" points="1648.85,-1087 1648.85,-1111.4 1814.89,-1111.4 1814.89,-1087 1648.85,-1087"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1087 1648.85,-1111.4 1814.89,-1111.4 1814.89,-1087 1648.85,-1087"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1694.19" y="-1096.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">articlehistory</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1060.2 1648.85,-1087 1814.89,-1087 1814.89,-1060.2 1648.85,-1060.2"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1060.2 1648.85,-1087 1814.89,-1087 1814.89,-1060.2 1648.85,-1060.2"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1070.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.39" y="-1071.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.39" y="-1071.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1033.4 1648.85,-1060.2 1814.89,-1060.2 1814.89,-1033.4 1648.85,-1033.4"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1033.4 1648.85,-1060.2 1814.89,-1060.2 1814.89,-1033.4 1648.85,-1033.4"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1043.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">article_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1045.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1045.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
|
||||
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1006.6 1648.85,-1033.4 1814.89,-1033.4 1814.89,-1006.6 1648.85,-1006.6"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1006.6 1648.85,-1033.4 1814.89,-1033.4 1814.89,-1006.6 1648.85,-1006.6"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1016.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">companyuser_id</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1765.11" y="-1016.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1018.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1018.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
|
||||
<polygon fill="#272e3d" stroke="none" points="1648.85,-979.8 1648.85,-1006.6 1814.89,-1006.6 1814.89,-979.8 1648.85,-979.8"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-979.8 1648.85,-1006.6 1814.89,-1006.6 1814.89,-979.8 1648.85,-979.8"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-987.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">date</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1759.39" y="-989.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
|
||||
<polygon fill="#2a3242" stroke="none" points="1648.85,-953 1648.85,-979.8 1814.89,-979.8 1814.89,-953 1648.85,-953"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-953 1648.85,-979.8 1814.89,-979.8 1814.89,-953 1648.85,-953"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-960.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">text</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-962.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
|
||||
<polygon fill="#272e3d" stroke="none" points="1648.85,-926.2 1648.85,-953 1814.89,-953 1814.89,-926.2 1648.85,-926.2"/>
|
||||
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-926.2 1648.85,-953 1814.89,-953 1814.89,-926.2 1648.85,-926.2"/>
|
||||
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-934" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">meta?</text>
|
||||
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-936" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
|
||||
</g>
|
||||
<!-- articlehistory->companyuser -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>articlehistory:e->companyuser:n</title>
|
||||
<g id="a_edge28"><a xlink:title="FK_articlehistory_companyuser">
|
||||
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M1815.89,-1006.6C1875.29,-1006.6 1809.1,-914.69 1856.87,-879.4 2089.17,-707.8 6820.72,-979.05 7054.87,-810 7120.81,-762.4 7117.3,-718.3 7116.98,-639.55"/>
|
||||
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M1815.89,-1020C1880.98,-1020 1805.07,-918.81 1856.87,-879.4 2086.74,-704.56 6820.72,-979.05 7054.87,-810 7120.81,-762.4 7117.3,-718.3 7116.98,-639.55"/>
|
||||
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="7119.43,-639.91 7116.96,-632.92 7114.53,-639.92 7119.43,-639.91"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
<!-- articlehistory->article -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>articlehistory:sw->article:n</title>
|
||||
<title>articlehistory:se->article:n</title>
|
||||
<g id="a_edge27"><a xlink:title="FK_articlehistory_article">
|
||||
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M1647.85,-1020C1621.33,-993.47 1627.31,-966.31 1648.85,-935.6 1694.51,-870.52 1974.73,-886.23 1995.47,-813.78"/>
|
||||
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="1997.86,-814.37 1996.39,-807.1 1993.01,-813.7 1997.86,-814.37"/>
|
||||
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M1815.89,-1033.4C1865.98,-983.32 1812.84,-934.87 1856.87,-879.4 1898.67,-826.74 1988.16,-871.01 1996.11,-813.77"/>
|
||||
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="1998.54,-814.26 1996.55,-807.11 1993.65,-813.94 1998.54,-814.26"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 321 KiB |
94
scripts/test-mcp.js
Normal file
94
scripts/test-mcp.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert';
|
||||
import LibDashStore from '../src/index.js';
|
||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||
|
||||
async function main() {
|
||||
console.log('Testing MCP store methods on in-memory SQLite...');
|
||||
const provider = new BetterSqlite3Provider({ path: ':memory:' });
|
||||
await provider.initialize();
|
||||
const store = await new LibDashStore(provider);
|
||||
|
||||
// 1. Create a google_auth user
|
||||
const googleAuthId = await store.google_auth.add('mcp-tester@example.com');
|
||||
assert(googleAuthId > 0, 'googleAuthId should be positive integer');
|
||||
|
||||
// 2. Register client
|
||||
const client = await store.mcp.registerClient({
|
||||
clientId: 'test-client-123',
|
||||
clientSecret: 'secret-456',
|
||||
clientName: 'Test Client',
|
||||
redirectUris: ['http://localhost:3000/callback'],
|
||||
grantTypes: ['authorization_code', 'refresh_token'],
|
||||
responseTypes: ['code'],
|
||||
scope: 'mcp:tools',
|
||||
});
|
||||
console.log('Registered client:', client);
|
||||
assert.strictEqual(client.clientId, 'test-client-123');
|
||||
assert.strictEqual(client.clientName, 'Test Client');
|
||||
|
||||
// 3. Get client
|
||||
const fetched = await store.mcp.getClient('test-client-123');
|
||||
assert(fetched, 'Should find client');
|
||||
assert.strictEqual(fetched.clientId, 'test-client-123');
|
||||
|
||||
// 4. List clients
|
||||
const clients = await store.mcp.listClients();
|
||||
assert.strictEqual(clients.length, 1);
|
||||
|
||||
// 5. Create auth code
|
||||
const code = 'auth-code-xyz';
|
||||
await store.mcp.createAuthCode({
|
||||
code,
|
||||
clientId: 'test-client-123',
|
||||
googleAuthId,
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
codeChallenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
|
||||
codeChallengeMethod: 'S256',
|
||||
scope: 'mcp:tools',
|
||||
expiresAt: new Date(Date.now() + 300_000),
|
||||
});
|
||||
|
||||
// 6. Get auth code
|
||||
const codeRow = await store.mcp.getAuthCode(code);
|
||||
assert(codeRow, 'Should find auth code');
|
||||
assert.strictEqual(codeRow.clientId, 'test-client-123');
|
||||
assert.strictEqual(codeRow.used, 0);
|
||||
|
||||
// 7. Consume auth code
|
||||
await store.mcp.consumeAuthCode(code);
|
||||
const consumedRow = await store.mcp.getAuthCode(code);
|
||||
assert.strictEqual(consumedRow.used, 1);
|
||||
|
||||
// 8. Create refresh token
|
||||
const refreshToken = 'refresh-token-abc';
|
||||
await store.mcp.createRefreshToken({
|
||||
refreshToken,
|
||||
clientId: 'test-client-123',
|
||||
googleAuthId,
|
||||
scope: 'mcp:tools',
|
||||
expiresAt: new Date(Date.now() + 86400_000),
|
||||
});
|
||||
|
||||
// 9. Get refresh token
|
||||
const tokenRow = await store.mcp.getRefreshToken(refreshToken);
|
||||
assert(tokenRow, 'Should find refresh token');
|
||||
assert.strictEqual(tokenRow.revoked, 0);
|
||||
|
||||
// 10. Revoke refresh token
|
||||
await store.mcp.revokeRefreshToken(refreshToken);
|
||||
const revokedRow = await store.mcp.getRefreshToken(refreshToken);
|
||||
assert.strictEqual(revokedRow.revoked, 1);
|
||||
|
||||
// 11. Delete client
|
||||
await store.mcp.deleteClient('test-client-123');
|
||||
const deleted = await store.mcp.getClient('test-client-123');
|
||||
assert.strictEqual(deleted, null);
|
||||
|
||||
await provider.close();
|
||||
console.log('✓ All MCP store tests passed successfully!');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -33,6 +33,7 @@ async function run() {
|
||||
|
||||
const list = await store.modelconversation.list(userA);
|
||||
assert.equal(list.length, 1);
|
||||
assert.equal(list[0].first_user_message, null);
|
||||
|
||||
assert.deepEqual(await store.modelmessage.list(userA, conv.id), []);
|
||||
|
||||
@@ -65,6 +66,8 @@ async function run() {
|
||||
const msgs = await store.modelmessage.list(userA, conv.id);
|
||||
assert.deepEqual(msgs.map((m) => m.message), ['hello', 'echo: hello', 'second']);
|
||||
assert.deepEqual(msgs.map((m) => m.role), ['user', 'assistant', 'user']);
|
||||
assert.equal((await store.modelconversation.list(userA)).find((c) => c.id === conv.id).first_user_message, 'hello');
|
||||
assert.equal((await store.modelconversation.get(userA, conv.id)).first_user_message, 'hello');
|
||||
|
||||
// Other user cannot see or write
|
||||
const userB = await store.google_auth.add('mc-b@example.com');
|
||||
@@ -74,6 +77,33 @@ async function run() {
|
||||
await assert.rejects(store.modelmessage.list(userB, conv.id), /Conversation not found/);
|
||||
await assert.rejects(store.modelmessage.add(userB, conv.id, { message: 'x' }), /Conversation not found/);
|
||||
|
||||
// started_by: top-level is null, subagents link to their originator, list exposes it
|
||||
assert.equal(conv.started_by, null);
|
||||
const sub1 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: conv.id });
|
||||
assert.equal(sub1.started_by, conv.id);
|
||||
const sub2 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
|
||||
const sibling = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm' });
|
||||
await assert.rejects(
|
||||
store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: 0 }),
|
||||
/Invalid started_by/,
|
||||
);
|
||||
const listed = await store.modelconversation.list(userA);
|
||||
assert.equal(listed.find((c) => c.id === sub2.id).started_by, sub1.id);
|
||||
assert.equal((await store.modelconversation.get(userA, sub1.id)).started_by, conv.id);
|
||||
await store.modelmessage.add(userA, sub2.id, { message: 'deep' });
|
||||
|
||||
// Deleting a child removes only its own chain; deleting the root removes the whole chain
|
||||
await store.modelconversation.delete(userA, sub2.id);
|
||||
await assert.rejects(store.modelconversation.get(userA, sub2.id), /Conversation not found/);
|
||||
await store.modelconversation.get(userA, sub1.id);
|
||||
const sub3 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
|
||||
await store.modelconversation.delete(userA, conv.id);
|
||||
for (const gone of [conv.id, sub1.id, sub3.id]) {
|
||||
await assert.rejects(store.modelconversation.get(userA, gone), /Conversation not found/);
|
||||
}
|
||||
await store.modelconversation.get(userA, sibling.id);
|
||||
await assert.rejects(store.modelconversation.delete(userA, conv.id), /Conversation not found/);
|
||||
|
||||
console.log('PASS');
|
||||
} finally {
|
||||
await provider.close?.();
|
||||
|
||||
205
scripts/test-order-filters.js
Normal file
205
scripts/test-order-filters.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import LibDashStore from '../src/index.js';
|
||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||
|
||||
async function run() {
|
||||
console.log('--- Testing Order Filtering & Paging in dashdbschema ---');
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'test-order-filters-'));
|
||||
const dbFile = join(tempDir, 'test.db');
|
||||
const storageDir = join(tempDir, 'storage');
|
||||
|
||||
const provider = new BetterSqlite3Provider({
|
||||
filename: dbFile,
|
||||
storageDir,
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
const store = await new LibDashStore(provider);
|
||||
|
||||
try {
|
||||
const googleAuthId = await store.google_auth.add('filter-test@example.com');
|
||||
const company = await store.company.create({
|
||||
name: 'Filter Test Corp',
|
||||
iso639: 'de',
|
||||
taxrate: 19,
|
||||
reducedtaxrate: 7,
|
||||
googleAuthId,
|
||||
});
|
||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||
|
||||
const warehouse = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||
await store.unit.upsert(googleAuthId, {
|
||||
si: 'stk',
|
||||
basesi: 'stk',
|
||||
basesifactor: 1,
|
||||
symbol: 'Stk',
|
||||
name: 'Stück',
|
||||
});
|
||||
|
||||
const groups = await store.customergroup.list(googleAuthId);
|
||||
const customerAlice = await store.entity.create(googleAuthId, {
|
||||
customergroupId: groups[0].id,
|
||||
firstName: 'Alice',
|
||||
name: 'Wonderland',
|
||||
});
|
||||
const customerBob = await store.entity.create(googleAuthId, {
|
||||
customergroupId: groups[0].id,
|
||||
firstName: 'Bob',
|
||||
name: 'Builder',
|
||||
});
|
||||
|
||||
const art = await store.article.createUnderParent({
|
||||
googleAuthId,
|
||||
parentId: company.rootCategoryId,
|
||||
name: 'Widget',
|
||||
track_stock: 1,
|
||||
});
|
||||
await store.stock.adjust(googleAuthId, {
|
||||
sku: art.sku,
|
||||
warehouseId: warehouse.id,
|
||||
amount: 100,
|
||||
text: 'Stock in',
|
||||
});
|
||||
|
||||
// Account for payments
|
||||
const account = await store.account.create(googleAuthId, {
|
||||
name: 'Bank',
|
||||
type: 'bank',
|
||||
});
|
||||
|
||||
// Create 3 orders
|
||||
// Order 1: Alice, O-1001, €119 (1 * 100 + 19% tax), Date: 2 months ago (2026-08-01)
|
||||
// Paid in full, Unshipped
|
||||
const order1 = await store.order.create(googleAuthId, {
|
||||
number: 'O-1001',
|
||||
date: '2026-08-01T10:00:00Z',
|
||||
entityId: customerAlice.id,
|
||||
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 100, taxrate: 19 }],
|
||||
});
|
||||
await store.payment.create(googleAuthId, {
|
||||
accountId: account.id,
|
||||
manifestId: order1.id,
|
||||
type: 'bank_transfer',
|
||||
amount: 119,
|
||||
});
|
||||
|
||||
// Order 2: Bob, O-1002, €238 (2 * 100 + 19% tax), Date: 10 days ago (2026-10-03)
|
||||
// Unpaid, Shipped in full with shipping 'shipped'
|
||||
const order2 = await store.order.create(googleAuthId, {
|
||||
number: 'O-1002',
|
||||
date: '2026-10-03T10:00:00Z',
|
||||
entityId: customerBob.id,
|
||||
lines: [{ sku: art.sku, name: 'Widget', amount: 2, net: 100, taxrate: 19 }],
|
||||
});
|
||||
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||
orderId: order2.id,
|
||||
lines: [{ sku: art.sku, amount: 2 }],
|
||||
});
|
||||
await store.shipping.create(googleAuthId, {
|
||||
manifestId: shipment2.id,
|
||||
type: 'parcel',
|
||||
state: 'shipped',
|
||||
});
|
||||
|
||||
// Order 3: Alice, O-1003, €59.50, Date: today (2026-10-13)
|
||||
// Unpaid, Undelivered
|
||||
const order3 = await store.order.create(googleAuthId, {
|
||||
number: 'O-1003',
|
||||
date: '2026-10-13T10:00:00Z',
|
||||
entityId: customerAlice.id,
|
||||
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 50, taxrate: 19 }],
|
||||
});
|
||||
|
||||
// Order 4: Bob, O-1004, €119 (1 * 100 + 19% tax), Date: today (2026-10-13)
|
||||
// Overpaid (paid €150), Delivered
|
||||
const order4 = await store.order.create(googleAuthId, {
|
||||
number: 'O-1004',
|
||||
date: '2026-10-13T10:00:00Z',
|
||||
entityId: customerBob.id,
|
||||
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 100, taxrate: 19 }],
|
||||
});
|
||||
await store.payment.create(googleAuthId, {
|
||||
accountId: account.id,
|
||||
manifestId: order4.id,
|
||||
type: 'bank_transfer',
|
||||
amount: 150, // Overpaid!
|
||||
});
|
||||
const shipment4 = await store.shipment.create(googleAuthId, {
|
||||
orderId: order4.id,
|
||||
lines: [{ sku: art.sku, amount: 1 }],
|
||||
});
|
||||
await store.shipping.create(googleAuthId, {
|
||||
manifestId: shipment4.id,
|
||||
type: 'parcel',
|
||||
state: 'shipped',
|
||||
});
|
||||
|
||||
console.log('Orders created successfully.');
|
||||
|
||||
// 1. Search Alice
|
||||
const resAlice = await store.order.list(googleAuthId, { search: 'Alice' });
|
||||
assert.equal(resAlice.total, 2, 'Should find 2 orders for Alice');
|
||||
assert.deepEqual(resAlice.orders.map((o) => o.number), ['O-1003', 'O-1001']);
|
||||
|
||||
// 2. Search Bob
|
||||
const resBob = await store.order.list(googleAuthId, { search: 'Bob' });
|
||||
assert.equal(resBob.total, 2, 'Should find 2 orders for Bob');
|
||||
assert.deepEqual(resBob.orders.map((o) => o.number), ['O-1004', 'O-1002']);
|
||||
|
||||
// 3. Search 1002
|
||||
const res1002 = await store.order.list(googleAuthId, { search: '1002' });
|
||||
assert.equal(res1002.total, 1, 'Should find 1 order for 1002');
|
||||
assert.equal(res1002.orders[0].number, 'O-1002');
|
||||
|
||||
// 4. onlyUnpaid (O-1004 is overpaid, so it also counts as unpaid!)
|
||||
const resUnpaid = await store.order.list(googleAuthId, { onlyUnpaid: true });
|
||||
assert.equal(resUnpaid.total, 3, 'O-1004 (overpaid), O-1003 (unpaid), O-1002 (unpaid) are unpaid');
|
||||
assert.deepEqual(resUnpaid.orders.map((o) => o.number), ['O-1004', 'O-1003', 'O-1002']);
|
||||
|
||||
// 5. onlyUndelivered
|
||||
const resUndelivered = await store.order.list(googleAuthId, { onlyUndelivered: true });
|
||||
assert.equal(resUndelivered.total, 2, 'O-1001 and O-1003 are undelivered');
|
||||
assert.deepEqual(resUndelivered.orders.map((o) => o.number), ['O-1003', 'O-1001']);
|
||||
|
||||
// 6. onlyUnpaid AND onlyUndelivered
|
||||
const resBoth = await store.order.list(googleAuthId, { onlyUnpaid: true, onlyUndelivered: true });
|
||||
assert.equal(resBoth.total, 1, 'Only O-1003 is both unpaid and undelivered');
|
||||
assert.equal(resBoth.orders[0].number, 'O-1003');
|
||||
|
||||
// 7. seekMonths -1 (orders older than 1 month)
|
||||
const resSeek = await store.order.list(googleAuthId, { seekMonths: -1 });
|
||||
assert.equal(resSeek.total, 1, 'Only O-1001 is older than 1 month');
|
||||
assert.equal(resSeek.orders[0].number, 'O-1001');
|
||||
|
||||
// 8. Paging
|
||||
const resPaged = await store.order.list(googleAuthId, { offset: 0, limit: 1 });
|
||||
assert.equal(resPaged.total, 4);
|
||||
assert.equal(resPaged.orders.length, 1);
|
||||
assert.equal(resPaged.orders[0].number, 'O-1004');
|
||||
|
||||
const resPaged2 = await store.order.list(googleAuthId, { offset: 1, limit: 1 });
|
||||
assert.equal(resPaged2.total, 4);
|
||||
assert.equal(resPaged2.orders.length, 1);
|
||||
assert.equal(resPaged2.orders[0].number, 'O-1003');
|
||||
|
||||
// 9. Unpaged backwards-compatibility
|
||||
const resAll = await store.order.list(googleAuthId);
|
||||
assert(Array.isArray(resAll));
|
||||
assert.equal(resAll.length, 4);
|
||||
|
||||
console.log('✓ All filter and pagination tests passed via store.order.list!');
|
||||
} finally {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -106,10 +106,17 @@ async function run() {
|
||||
});
|
||||
assert.equal(order.lines.length, 2);
|
||||
|
||||
// 4. Create shipping with state 'open' (not shipped yet)
|
||||
console.log('4. Creating shipping with state "open"...');
|
||||
// 4. Create shipment and shipping with state 'open' (not shipped yet)
|
||||
console.log('4. Creating shipment and shipping with state "open"...');
|
||||
const shipment = await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [
|
||||
{ sku: trackedArt.sku, amount: 3 },
|
||||
{ sku: untrackedArt.sku, amount: 2 },
|
||||
],
|
||||
});
|
||||
const shippingOpen = await store.shipping.create(googleAuthId, {
|
||||
manifestId: order.id,
|
||||
manifestId: shipment.id,
|
||||
type: 'parcel',
|
||||
state: 'open',
|
||||
});
|
||||
@@ -145,8 +152,13 @@ async function run() {
|
||||
// Assert article history: order goes into history, shipment does NOT
|
||||
const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
|
||||
console.log('Tracked article history:', history.map((h) => h.text));
|
||||
const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3)');
|
||||
assert(orderHistory, 'Article history should contain order creation entry');
|
||||
const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)');
|
||||
assert(orderHistory, 'Article history should contain order creation entry with formatted gross price');
|
||||
assert(orderHistory.meta, 'Article history entry should have meta');
|
||||
const parsedMeta = JSON.parse(orderHistory.meta);
|
||||
assert.equal(parsedMeta.type, 'order');
|
||||
assert.equal(parsedMeta.orderId, order.id);
|
||||
assert.equal(parsedMeta.gross, 59.5);
|
||||
const shippingHistory = history.find((h) => h.text.includes('Versand Auftrag'));
|
||||
assert(!shippingHistory, 'Article history should NOT report the shipment');
|
||||
|
||||
@@ -154,14 +166,29 @@ async function run() {
|
||||
const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku);
|
||||
console.log('Untracked article history:', untrackedHistory.map((h) => h.text));
|
||||
assert(
|
||||
untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2)'),
|
||||
'Untracked article history should contain order creation entry',
|
||||
untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2, € 23,80)'),
|
||||
'Untracked article history should contain order creation entry with formatted gross price',
|
||||
);
|
||||
assert(
|
||||
!untrackedHistory.some((h) => h.text.includes('Versand Auftrag')),
|
||||
'Untracked article history should NOT report the shipment',
|
||||
);
|
||||
|
||||
// Test order import with historical date preserves date in article history
|
||||
const historicalDate = '2023-04-15T10:30:00.000Z';
|
||||
await store.order.create(googleAuthId, {
|
||||
number: 'O-HIST-001',
|
||||
date: historicalDate,
|
||||
createdAt: historicalDate,
|
||||
lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }],
|
||||
});
|
||||
const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
|
||||
const oldEntry = histWithOld.find((h) => h.text.startsWith('Auftrag O-HIST-001'));
|
||||
assert(oldEntry, 'Historical order entry should exist in article history');
|
||||
assert.equal(new Date(oldEntry.date).toISOString(), historicalDate, 'History entry should preserve historical order date');
|
||||
const oldMeta = JSON.parse(oldEntry.meta);
|
||||
assert.equal(oldMeta.orderNumber, 'O-HIST-001');
|
||||
|
||||
// 6. Test immutability: once shipped, cannot be deleted
|
||||
console.log('6. Testing cannot delete shipped shipping...');
|
||||
await assert.rejects(
|
||||
@@ -180,8 +207,8 @@ async function run() {
|
||||
{ message: 'Shipped shipping cannot be undone' },
|
||||
);
|
||||
|
||||
// 8. Test order cannot be shipped again
|
||||
console.log('8. Testing order cannot be shipped twice...');
|
||||
// 8. Test order manifest cannot be used directly for shipping
|
||||
console.log('8. Testing order manifest cannot be used directly for shipping...');
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await store.shipping.create(googleAuthId, {
|
||||
@@ -190,10 +217,10 @@ async function run() {
|
||||
state: 'shipped',
|
||||
});
|
||||
},
|
||||
{ message: 'Order already shipped' },
|
||||
{ message: 'Manifest is not a shipment' },
|
||||
);
|
||||
|
||||
// 9. Test direct creation with state 'shipped'
|
||||
// 9. Test direct creation with state 'shipped' on shipment manifest
|
||||
console.log('9. Testing direct creation with state "shipped"...');
|
||||
const order2 = await store.order.create(googleAuthId, {
|
||||
number: 'O-2026-002',
|
||||
@@ -211,8 +238,13 @@ async function run() {
|
||||
],
|
||||
});
|
||||
|
||||
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||
orderId: order2.id,
|
||||
lines: [{ sku: trackedArt.sku, amount: 2 }],
|
||||
});
|
||||
|
||||
const directShipped = await store.shipping.create(googleAuthId, {
|
||||
manifestId: order2.id,
|
||||
manifestId: shipment2.id,
|
||||
type: 'parcel',
|
||||
state: 'shipped',
|
||||
});
|
||||
|
||||
240
scripts/test-shipment.js
Normal file
240
scripts/test-shipment.js
Normal file
@@ -0,0 +1,240 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import LibDashStore from '../src/index.js';
|
||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||
|
||||
async function run() {
|
||||
console.log('--- Testing Shipment Manifest & Multi-Package Shipping ---');
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'test-shipment-'));
|
||||
const dbFile = join(tempDir, 'test.db');
|
||||
const storageDir = join(tempDir, 'storage');
|
||||
|
||||
const provider = new BetterSqlite3Provider({
|
||||
filename: dbFile,
|
||||
storageDir,
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
const store = await new LibDashStore(provider);
|
||||
|
||||
try {
|
||||
const googleAuthId = await store.google_auth.add('shipment-test@example.com');
|
||||
const company = await store.company.create({
|
||||
name: 'Shipment Test Corp',
|
||||
iso639: 'de',
|
||||
taxrate: 19,
|
||||
reducedtaxrate: 7,
|
||||
googleAuthId,
|
||||
});
|
||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||
|
||||
// Create a warehouse
|
||||
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||
|
||||
// Create customer entity
|
||||
const groups = await store.customergroup.list(googleAuthId);
|
||||
const customer = await store.entity.create(googleAuthId, {
|
||||
customergroupId: groups[0].id,
|
||||
name: 'Max Mustermann',
|
||||
});
|
||||
|
||||
// Create Item A and Item B
|
||||
const itemA = await store.article.createUnderParent({
|
||||
googleAuthId,
|
||||
parentId: company.rootCategoryId,
|
||||
name: 'Item A',
|
||||
sku: 'ITEM-A',
|
||||
});
|
||||
const itemB = await store.article.createUnderParent({
|
||||
googleAuthId,
|
||||
parentId: company.rootCategoryId,
|
||||
name: 'Item B',
|
||||
sku: 'ITEM-B',
|
||||
});
|
||||
|
||||
// Stock up Item A (20) and Item B (20)
|
||||
await store.stock.adjust(googleAuthId, {
|
||||
sku: 'ITEM-A',
|
||||
warehouseId: wh.id,
|
||||
amount: 20,
|
||||
comment: 'Initial Stock',
|
||||
});
|
||||
await store.stock.adjust(googleAuthId, {
|
||||
sku: 'ITEM-B',
|
||||
warehouseId: wh.id,
|
||||
amount: 20,
|
||||
comment: 'Initial Stock',
|
||||
});
|
||||
|
||||
// Create Order with 10x Item A, 5x Item B
|
||||
const order = await store.order.create(googleAuthId, {
|
||||
number: 'AU-1001',
|
||||
date: '2026-10-12',
|
||||
entityId: customer.id,
|
||||
lines: [
|
||||
{ sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 },
|
||||
{ sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 },
|
||||
],
|
||||
});
|
||||
assert.equal(order.number, 'AU-1001');
|
||||
console.log('Order created:', order.number);
|
||||
|
||||
// 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1
|
||||
const shipment1 = await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [
|
||||
{ sku: 'ITEM-A', amount: 6 },
|
||||
{ sku: 'ITEM-B', amount: 3 },
|
||||
],
|
||||
});
|
||||
assert.equal(shipment1.number, 'AU-1001-L-1');
|
||||
assert.equal(shipment1.lines.length, 2);
|
||||
console.log('PASS: Shipment 1 created with number:', shipment1.number);
|
||||
|
||||
// 2. Create Shipment 2: 5x Item A -> Fails with ceiling violation (6 + 5 > 10)
|
||||
let ceilingErrorThrown = false;
|
||||
try {
|
||||
await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [{ sku: 'ITEM-A', amount: 5 }],
|
||||
});
|
||||
} catch (err) {
|
||||
ceilingErrorThrown = true;
|
||||
assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/);
|
||||
console.log('PASS: Ceiling violation caught:', err.message);
|
||||
}
|
||||
assert.ok(ceilingErrorThrown, 'Expected ceiling violation error to be thrown');
|
||||
|
||||
// Also test foreign article rejection
|
||||
let foreignArticleErrorThrown = false;
|
||||
try {
|
||||
await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [{ sku: 'ITEM-UNKNOWN', amount: 1 }],
|
||||
});
|
||||
} catch (err) {
|
||||
foreignArticleErrorThrown = true;
|
||||
assert.match(err.message, /Article \(ITEM-UNKNOWN\) is not part of order/);
|
||||
console.log('PASS: Foreign article rejected:', err.message);
|
||||
}
|
||||
assert.ok(foreignArticleErrorThrown, 'Expected foreign article error to be thrown');
|
||||
|
||||
// 3. Create Shipment 2: 4x Item A, 2x Item B -> AU-1001-L-2
|
||||
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [
|
||||
{ sku: 'ITEM-A', amount: 4 },
|
||||
{ sku: 'ITEM-B', amount: 2 },
|
||||
],
|
||||
});
|
||||
assert.equal(shipment2.number, 'AU-1001-L-2');
|
||||
assert.equal(shipment2.lines.length, 2);
|
||||
console.log('PASS: Shipment 2 created with number:', shipment2.number);
|
||||
|
||||
// 4. Try creating another shipment (now 10/10 of Item A and 5/5 of Item B are shipped)
|
||||
let exhaustedErrorThrown = false;
|
||||
try {
|
||||
await store.shipment.create(googleAuthId, {
|
||||
orderId: order.id,
|
||||
lines: [{ sku: 'ITEM-A', amount: 1 }],
|
||||
});
|
||||
} catch (err) {
|
||||
exhaustedErrorThrown = true;
|
||||
assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/);
|
||||
console.log('PASS: Exhausted quantity violation caught:', err.message);
|
||||
}
|
||||
assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown');
|
||||
|
||||
// 5. Add packages to Shipment 1 with tracking codes
|
||||
const pkg1 = await store.shipping.create(googleAuthId, {
|
||||
manifestId: shipment1.id,
|
||||
type: 'parcel',
|
||||
state: 'draft',
|
||||
trackingCode: 'TRACK-111',
|
||||
carrier: 'DHL',
|
||||
});
|
||||
assert.equal(pkg1.trackingCode, 'TRACK-111');
|
||||
assert.equal(pkg1.carrier, 'DHL');
|
||||
assert.equal(pkg1.state, 'draft');
|
||||
console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode);
|
||||
|
||||
const pkg2 = await store.shipping.create(googleAuthId, {
|
||||
manifestId: shipment1.id,
|
||||
type: 'parcel',
|
||||
state: 'draft',
|
||||
trackingCode: 'TRACK-222',
|
||||
carrier: 'DHL',
|
||||
});
|
||||
assert.equal(pkg2.trackingCode, 'TRACK-222');
|
||||
console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode);
|
||||
|
||||
// Check listByOrderId
|
||||
const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id);
|
||||
assert.equal(orderShipments.length, 2);
|
||||
assert.equal(orderShipments[0].shippings.length, 2);
|
||||
console.log('PASS: store.shipment.listByOrderId returned both shipments');
|
||||
|
||||
// Check getById
|
||||
const fetchedShipment1 = await store.shipment.getById(googleAuthId, shipment1.id);
|
||||
assert.equal(fetchedShipment1.id, shipment1.id);
|
||||
assert.equal(fetchedShipment1.shippings.length, 2);
|
||||
console.log('PASS: store.shipment.getById returned shipment with shippings');
|
||||
|
||||
// 6. Transition Package 1 to shipped (should deduct ledger stock for Shipment 1 lines: 6 of Item A, 3 of Item B)
|
||||
const updatedPkg1 = await store.shipping.update(googleAuthId, pkg1.id, {
|
||||
state: 'shipped',
|
||||
});
|
||||
assert.equal(updatedPkg1.state, 'shipped');
|
||||
console.log('PASS: Package 1 transitioned to shipped');
|
||||
|
||||
// Transition Package 2 to shipped (stock already booked for shipment1, should not double-deduct)
|
||||
const updatedPkg2 = await store.shipping.update(googleAuthId, pkg2.id, {
|
||||
state: 'shipped',
|
||||
});
|
||||
assert.equal(updatedPkg2.state, 'shipped');
|
||||
console.log('PASS: Package 2 transitioned to shipped');
|
||||
|
||||
// 7. Verify cannot delete shipped shipment
|
||||
let deleteErrorThrown = false;
|
||||
try {
|
||||
await store.shipment.delete(googleAuthId, shipment1.id);
|
||||
} catch (err) {
|
||||
deleteErrorThrown = true;
|
||||
assert.match(err.message, /Shipped shipment cannot be deleted/);
|
||||
console.log('PASS: Shipped shipment delete rejected:', err.message);
|
||||
}
|
||||
assert.ok(deleteErrorThrown, 'Expected shipped shipment delete error');
|
||||
|
||||
// 8. Delete unshipped shipment 2
|
||||
await store.shipment.delete(googleAuthId, shipment2.id);
|
||||
const shipmentsAfterDel = await store.shipment.listByOrderId(googleAuthId, order.id);
|
||||
assert.equal(shipmentsAfterDel.length, 1);
|
||||
console.log('PASS: Unshipped shipment 2 successfully deleted');
|
||||
|
||||
// 9. Verify peek next number works for entity and other types without error
|
||||
const nextEntity = await store.entity.peekNextNumber(googleAuthId);
|
||||
assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0);
|
||||
console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity);
|
||||
|
||||
const numList = await store.numbers.list(googleAuthId);
|
||||
assert.ok(Array.isArray(numList) && numList.length > 0);
|
||||
const shipmentSeq = numList.find((n) => n.type === 'shipment');
|
||||
assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list');
|
||||
assert.equal(shipmentSeq.prefix, '-L-');
|
||||
console.log('PASS: store.numbers.list includes shipment with prefix -L-');
|
||||
|
||||
console.log('--- ALL TESTS PASSED SUCCESSFULLY! ---');
|
||||
} finally {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error('TEST FAILED:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
40
src/index.js
40
src/index.js
@@ -98,6 +98,7 @@ import orderGetById from './store/order/getById.js';
|
||||
import orderCreate from './store/order/create.js';
|
||||
import orderUpdate from './store/order/update.js';
|
||||
import orderDelete from './store/order/delete.js';
|
||||
import orderRebuildSummaries from './store/order/rebuildSummaries.js';
|
||||
import posterminalList from './store/posterminal/list.js';
|
||||
import posterminalUpsert from './store/posterminal/upsert.js';
|
||||
import posterminalDelete from './store/posterminal/delete.js';
|
||||
@@ -185,6 +186,10 @@ import shippingCreate from './store/shipping/create.js';
|
||||
import shippingUpdate from './store/shipping/update.js';
|
||||
import shippingListByManifestId from './store/shipping/listByManifestId.js';
|
||||
import shippingDelete from './store/shipping/delete.js';
|
||||
import shipmentCreate from './store/shipment/create.js';
|
||||
import shipmentGetById from './store/shipment/getById.js';
|
||||
import shipmentListByOrderId from './store/shipment/listByOrderId.js';
|
||||
import shipmentDelete from './store/shipment/delete.js';
|
||||
import packageCreate from './store/package/create.js';
|
||||
import packageListByShippingId from './store/package/listByShippingId.js';
|
||||
import packageDelete from './store/package/delete.js';
|
||||
@@ -226,6 +231,18 @@ import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
|
||||
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
|
||||
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
|
||||
import optionslistSetOptions from './store/optionslist/setOptionsByArticleSku.js';
|
||||
import mcpRegisterClient from './store/mcp/registerClient.js';
|
||||
import mcpGetClient from './store/mcp/getClient.js';
|
||||
import mcpListClients from './store/mcp/listClients.js';
|
||||
import mcpDeleteClient from './store/mcp/deleteClient.js';
|
||||
import mcpCreateAuthCode from './store/mcp/createAuthCode.js';
|
||||
import mcpGetAuthCode from './store/mcp/getAuthCode.js';
|
||||
import mcpConsumeAuthCode from './store/mcp/consumeAuthCode.js';
|
||||
import mcpCreateRefreshToken from './store/mcp/createRefreshToken.js';
|
||||
import mcpGetRefreshToken from './store/mcp/getRefreshToken.js';
|
||||
import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js';
|
||||
import mcpListAuthorizations from './store/mcp/listAuthorizations.js';
|
||||
import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js';
|
||||
|
||||
export default class LibDashStore {
|
||||
/**
|
||||
@@ -411,6 +428,7 @@ export default class LibDashStore {
|
||||
create: orderCreate(provider),
|
||||
update: orderUpdate(provider),
|
||||
delete: orderDelete(provider),
|
||||
rebuildSummaries: orderRebuildSummaries(provider),
|
||||
};
|
||||
|
||||
this.posterminal = {
|
||||
@@ -457,6 +475,13 @@ export default class LibDashStore {
|
||||
delete: shippingDelete(provider),
|
||||
};
|
||||
|
||||
this.shipment = {
|
||||
create: shipmentCreate(provider),
|
||||
getById: shipmentGetById(provider),
|
||||
listByOrderId: shipmentListByOrderId(provider),
|
||||
delete: shipmentDelete(provider),
|
||||
};
|
||||
|
||||
this.package = {
|
||||
create: packageCreate(provider),
|
||||
listByShippingId: packageListByShippingId(provider),
|
||||
@@ -608,6 +633,21 @@ export default class LibDashStore {
|
||||
setOptionsByArticleSku: optionslistSetOptions(provider),
|
||||
};
|
||||
|
||||
this.mcp = {
|
||||
registerClient: mcpRegisterClient(provider),
|
||||
getClient: mcpGetClient(provider),
|
||||
listClients: mcpListClients(provider),
|
||||
deleteClient: mcpDeleteClient(provider),
|
||||
createAuthCode: mcpCreateAuthCode(provider),
|
||||
getAuthCode: mcpGetAuthCode(provider),
|
||||
consumeAuthCode: mcpConsumeAuthCode(provider),
|
||||
createRefreshToken: mcpCreateRefreshToken(provider),
|
||||
getRefreshToken: mcpGetRefreshToken(provider),
|
||||
revokeRefreshToken: mcpRevokeRefreshToken(provider),
|
||||
listAuthorizations: mcpListAuthorizations(provider),
|
||||
revokeAuthorizations: mcpRevokeAuthorizations(provider),
|
||||
};
|
||||
|
||||
this.purge = storePurge(provider);
|
||||
this.initialize = storeInitialize(provider);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import sql from 'mssql';
|
||||
import { logSlowQuery } from '../provider.js';
|
||||
|
||||
function normalizeRows(result) {
|
||||
const recordset = result?.recordset ?? [];
|
||||
@@ -24,11 +25,16 @@ export class MssqlExecutor {
|
||||
|
||||
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
||||
async query(sql, params = {}) {
|
||||
const req = this.#requestFactory();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
req.input(key, value);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const req = this.#requestFactory();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
req.input(key, value);
|
||||
}
|
||||
return normalizeRows(await req.query(sql));
|
||||
} finally {
|
||||
logSlowQuery(sql, performance.now() - start, params);
|
||||
}
|
||||
return normalizeRows(await req.query(sql));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,22 +43,27 @@ export class MssqlExecutor {
|
||||
* (`getnextnumberwithprefixandpadding`).
|
||||
*/
|
||||
async exec(procName, { inputs = {}, outputs = [] } = {}) {
|
||||
const req = this.#requestFactory();
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
req.input(key, value);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const req = this.#requestFactory();
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
req.input(key, value);
|
||||
}
|
||||
for (const out of outputs) {
|
||||
req.output(out);
|
||||
}
|
||||
const result = await req.execute(procName);
|
||||
return {
|
||||
rows: result.recordset ?? [],
|
||||
rowsAffected: Array.isArray(result.rowsAffected)
|
||||
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
||||
: 0,
|
||||
insertId: null,
|
||||
outputs: result.output ?? {},
|
||||
};
|
||||
} finally {
|
||||
logSlowQuery(`EXEC ${procName}`, performance.now() - start, inputs);
|
||||
}
|
||||
for (const out of outputs) {
|
||||
req.output(out);
|
||||
}
|
||||
const result = await req.execute(procName);
|
||||
return {
|
||||
rows: result.recordset ?? [],
|
||||
rowsAffected: Array.isArray(result.rowsAffected)
|
||||
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
||||
: 0,
|
||||
insertId: null,
|
||||
outputs: result.output ?? {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Drop order = reverse of schema create dependency order. */
|
||||
export const TABLES = [
|
||||
'ordersummary',
|
||||
'package',
|
||||
'shipping',
|
||||
'payment',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export const INSERT = `
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, [text])
|
||||
VALUES (@articleId, @companyuserId, @text)
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, [date], [text], meta)
|
||||
VALUES (@articleId, @companyuserId, COALESCE(@date, SYSUTCDATETIME()), @text, @meta)
|
||||
`;
|
||||
|
||||
export const LIST_BY_ARTICLE_ID = `
|
||||
SELECT id, companyuser_id AS companyuserId, [date], [text]
|
||||
SELECT id, companyuser_id AS companyuserId, [date], [text], meta
|
||||
FROM articlehistory
|
||||
WHERE article_id = @articleId
|
||||
ORDER BY [date] DESC, id DESC
|
||||
`;
|
||||
|
||||
export const LIST_BY_ARTICLE_ID_LIMITED = `
|
||||
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text]
|
||||
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text], meta
|
||||
FROM articlehistory
|
||||
WHERE article_id = @articleId
|
||||
ORDER BY [date] DESC, id DESC
|
||||
|
||||
@@ -19,7 +19,13 @@ SELECT
|
||||
p.hash AS fullHash,
|
||||
p.thumbw AS thumbW,
|
||||
p.thumbh AS thumbH,
|
||||
p.thumbhash AS thumbHash
|
||||
p.thumbhash AS thumbHash,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM categoryarticle ca
|
||||
INNER JOIN article a ON a.id = ca.article_id
|
||||
WHERE ca.category_id = t.id AND a.parentarticle_id IS NULL
|
||||
) AS articleCount
|
||||
FROM tree t
|
||||
LEFT JOIN categorytext ct
|
||||
ON ct.category_id = t.id AND ct.language_id = @languageId
|
||||
@@ -33,6 +39,19 @@ LEFT JOIN pic p ON p.id = first_pic.pic_id
|
||||
ORDER BY t.sort, t.id
|
||||
`;
|
||||
|
||||
export const COUNT_ARTICLES_IN_TREE = `
|
||||
WITH tree AS (
|
||||
SELECT id FROM category WHERE id = @rootId
|
||||
UNION ALL
|
||||
SELECT c.id FROM category c INNER JOIN tree t ON c.parent_id = t.id
|
||||
)
|
||||
SELECT COUNT(DISTINCT ca.article_id) AS total
|
||||
FROM categoryarticle ca
|
||||
INNER JOIN article a ON a.id = ca.article_id
|
||||
WHERE ca.category_id IN (SELECT id FROM tree)
|
||||
AND a.parentarticle_id IS NULL
|
||||
`;
|
||||
|
||||
import {
|
||||
ARTICLE_LIST_COLUMNS,
|
||||
ARTICLE_LIST_JOINS,
|
||||
|
||||
@@ -52,11 +52,13 @@ SELECT
|
||||
c.name,
|
||||
c.root_category_id AS rootCategoryId,
|
||||
c.default_language_id AS languageId,
|
||||
l.iso639,
|
||||
tr.rate AS taxrate,
|
||||
rtr.rate AS reducedtaxrate,
|
||||
tfr.rate AS taxfreerate
|
||||
FROM company c
|
||||
INNER JOIN companyuser cu ON cu.company_id = c.id
|
||||
LEFT JOIN language l ON l.id = c.default_language_id
|
||||
LEFT JOIN tax tr ON tr.id = c.taxrate
|
||||
LEFT JOIN tax rtr ON rtr.id = c.reducedtaxrate
|
||||
LEFT JOIN tax tfr ON tfr.id = c.taxfreerate
|
||||
|
||||
@@ -38,8 +38,8 @@ WHERE sku = @sku
|
||||
`;
|
||||
|
||||
export const INSERT_ARTICLE_HISTORY = `
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, [text])
|
||||
VALUES (@articleId, @companyuserId, @text);
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, [date], [text], meta)
|
||||
VALUES (@articleId, @companyuserId, COALESCE(@date, SYSUTCDATETIME()), @text, @meta);
|
||||
`;
|
||||
|
||||
export const GET_LATEST_ROW = `
|
||||
|
||||
@@ -57,13 +57,13 @@ WHERE id = @id
|
||||
`;
|
||||
|
||||
export const SHIPPING_CREATE = `
|
||||
INSERT INTO shipping (manifest_id, [number], [type], [state])
|
||||
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.[type], INSERTED.[state]
|
||||
VALUES (@manifestId, @number, @type, @state)
|
||||
INSERT INTO shipping (manifest_id, [number], tracking_code, carrier, [type], [state])
|
||||
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state]
|
||||
VALUES (@manifestId, @number, @trackingCode, @carrier, @type, @state)
|
||||
`;
|
||||
|
||||
export const SHIPPING_LIST_BY_MANIFEST = `
|
||||
SELECT id, manifest_id, [number], [type], [state]
|
||||
SELECT id, manifest_id, [number], tracking_code, carrier, [type], [state]
|
||||
FROM shipping
|
||||
WHERE manifest_id = @manifestId
|
||||
ORDER BY id ASC
|
||||
@@ -76,7 +76,7 @@ WHERE id = @id
|
||||
`;
|
||||
|
||||
export const GET_SHIPPING_BY_ID = `
|
||||
SELECT s.id, s.manifest_id AS manifestId, s.[number], s.[type], s.[state]
|
||||
SELECT s.id, s.manifest_id AS manifestId, s.[number], s.tracking_code, s.carrier, s.[type], s.[state]
|
||||
FROM shipping s
|
||||
INNER JOIN manifest m ON m.id = s.manifest_id
|
||||
WHERE s.id = @id AND m.company_id = @companyId;
|
||||
@@ -84,8 +84,8 @@ WHERE s.id = @id AND m.company_id = @companyId;
|
||||
|
||||
export const SHIPPING_UPDATE = `
|
||||
UPDATE shipping
|
||||
SET [type] = @type, [state] = @state
|
||||
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.[type], INSERTED.[state]
|
||||
SET [type] = @type, [state] = @state, tracking_code = @trackingCode, carrier = @carrier
|
||||
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state]
|
||||
WHERE id = @id;
|
||||
`;
|
||||
|
||||
|
||||
113
src/providers/mssql/queries/mcp.js
Normal file
113
src/providers/mssql/queries/mcp.js
Normal file
@@ -0,0 +1,113 @@
|
||||
export const INSERT_CLIENT = `
|
||||
INSERT INTO mcp_client (client_id, client_secret, client_name, redirect_uris, grant_types, response_types, scope)
|
||||
OUTPUT INSERTED.id, INSERTED.client_id, INSERTED.client_secret, INSERTED.client_name, INSERTED.redirect_uris, INSERTED.grant_types, INSERTED.response_types, INSERTED.scope, INSERTED.created_at
|
||||
VALUES (@clientId, @clientSecret, @clientName, @redirectUris, @grantTypes, @responseTypes, @scope)
|
||||
`;
|
||||
|
||||
export const GET_CLIENT = `
|
||||
SELECT
|
||||
id,
|
||||
client_id AS clientId,
|
||||
client_secret AS clientSecret,
|
||||
client_name AS clientName,
|
||||
redirect_uris AS redirectUris,
|
||||
grant_types AS grantTypes,
|
||||
response_types AS responseTypes,
|
||||
scope,
|
||||
created_at AS createdAt
|
||||
FROM mcp_client
|
||||
WHERE client_id = @clientId
|
||||
`;
|
||||
|
||||
export const LIST_CLIENTS = `
|
||||
SELECT
|
||||
id,
|
||||
client_id AS clientId,
|
||||
client_name AS clientName,
|
||||
redirect_uris AS redirectUris,
|
||||
grant_types AS grantTypes,
|
||||
response_types AS responseTypes,
|
||||
scope,
|
||||
created_at AS createdAt
|
||||
FROM mcp_client
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
export const DELETE_CLIENT = `
|
||||
DELETE FROM mcp_client
|
||||
WHERE client_id = @clientId
|
||||
`;
|
||||
|
||||
export const INSERT_AUTH_CODE = `
|
||||
INSERT INTO mcp_auth_code (code, client_id, google_auth_id, redirect_uri, code_challenge, code_challenge_method, scope, expires_at)
|
||||
VALUES (@code, @clientId, @googleAuthId, @redirectUri, @codeChallenge, @codeChallengeMethod, @scope, @expiresAt)
|
||||
`;
|
||||
|
||||
export const GET_AUTH_CODE = `
|
||||
SELECT
|
||||
code,
|
||||
client_id AS clientId,
|
||||
google_auth_id AS googleAuthId,
|
||||
redirect_uri AS redirectUri,
|
||||
code_challenge AS codeChallenge,
|
||||
code_challenge_method AS codeChallengeMethod,
|
||||
scope,
|
||||
expires_at AS expiresAt,
|
||||
used
|
||||
FROM mcp_auth_code
|
||||
WHERE code = @code
|
||||
`;
|
||||
|
||||
export const CONSUME_AUTH_CODE = `
|
||||
UPDATE mcp_auth_code
|
||||
SET used = 1
|
||||
WHERE code = @code
|
||||
`;
|
||||
|
||||
export const INSERT_REFRESH_TOKEN = `
|
||||
INSERT INTO mcp_token (refresh_token, client_id, google_auth_id, scope, expires_at)
|
||||
OUTPUT INSERTED.id
|
||||
VALUES (@refreshToken, @clientId, @googleAuthId, @scope, @expiresAt)
|
||||
`;
|
||||
|
||||
export const GET_REFRESH_TOKEN = `
|
||||
SELECT
|
||||
id,
|
||||
refresh_token AS refreshToken,
|
||||
client_id AS clientId,
|
||||
google_auth_id AS googleAuthId,
|
||||
scope,
|
||||
expires_at AS expiresAt,
|
||||
revoked
|
||||
FROM mcp_token
|
||||
WHERE refresh_token = @refreshToken
|
||||
`;
|
||||
|
||||
export const REVOKE_REFRESH_TOKEN = `
|
||||
UPDATE mcp_token
|
||||
SET revoked = 1
|
||||
WHERE refresh_token = @refreshToken
|
||||
`;
|
||||
|
||||
export const LIST_AUTHORIZATIONS_BY_USER = `
|
||||
SELECT
|
||||
c.id AS clientDbId,
|
||||
c.client_id AS clientId,
|
||||
c.client_name AS clientName,
|
||||
c.scope,
|
||||
c.created_at AS clientCreatedAt,
|
||||
MAX(t.created_at) AS authorizedAt,
|
||||
COUNT(t.id) AS tokenCount
|
||||
FROM mcp_token t
|
||||
JOIN mcp_client c ON t.client_id = c.client_id
|
||||
WHERE t.google_auth_id = @googleAuthId AND t.revoked = 0
|
||||
GROUP BY c.id, c.client_id, c.client_name, c.scope, c.created_at
|
||||
ORDER BY MAX(t.created_at) DESC
|
||||
`;
|
||||
|
||||
export const REVOKE_USER_AUTHORIZATIONS = `
|
||||
UPDATE mcp_token
|
||||
SET revoked = 1
|
||||
WHERE google_auth_id = @googleAuthId AND client_id = @clientId
|
||||
`;
|
||||
|
||||
@@ -5,29 +5,46 @@ export const MODELCONVERSATION_COLUMNS = `
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
state
|
||||
state,
|
||||
started_by
|
||||
`;
|
||||
|
||||
/** Raw stored payload of the conversation's first `user` message; `c` is the conversation alias. */
|
||||
const FIRST_USER_MESSAGE_COLUMN = `
|
||||
(
|
||||
SELECT m.message
|
||||
FROM modelmessage m
|
||||
WHERE m.id = (
|
||||
SELECT MIN(f.id)
|
||||
FROM modelmessage f
|
||||
WHERE f.conversation_id = c.id AND f.role = 'user'
|
||||
)
|
||||
) AS first_user_message
|
||||
`;
|
||||
|
||||
export const LIST_BY_COMPANYUSER = `
|
||||
SELECT
|
||||
${MODELCONVERSATION_COLUMNS}
|
||||
FROM modelconversation
|
||||
WHERE companyuser_id = @companyuserId
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
${MODELCONVERSATION_COLUMNS},
|
||||
${FIRST_USER_MESSAGE_COLUMN}
|
||||
FROM modelconversation c
|
||||
WHERE c.companyuser_id = @companyuserId
|
||||
ORDER BY c.updated_at DESC, c.id DESC
|
||||
`;
|
||||
|
||||
export const GET_BY_COMPANYUSER_AND_ID = `
|
||||
SELECT
|
||||
${MODELCONVERSATION_COLUMNS}
|
||||
FROM modelconversation
|
||||
WHERE id = @id AND companyuser_id = @companyuserId
|
||||
${MODELCONVERSATION_COLUMNS},
|
||||
${FIRST_USER_MESSAGE_COLUMN}
|
||||
FROM modelconversation c
|
||||
WHERE c.id = @id AND c.companyuser_id = @companyuserId
|
||||
`;
|
||||
|
||||
export const CREATE = `
|
||||
INSERT INTO modelconversation (
|
||||
companyuser_id,
|
||||
provider,
|
||||
model
|
||||
model,
|
||||
started_by
|
||||
)
|
||||
OUTPUT
|
||||
INSERTED.id,
|
||||
@@ -36,11 +53,13 @@ OUTPUT
|
||||
INSERTED.model,
|
||||
INSERTED.created_at,
|
||||
INSERTED.updated_at,
|
||||
INSERTED.state
|
||||
INSERTED.state,
|
||||
INSERTED.started_by
|
||||
VALUES (
|
||||
@companyuserId,
|
||||
@provider,
|
||||
@model
|
||||
@model,
|
||||
@startedBy
|
||||
)
|
||||
`;
|
||||
|
||||
@@ -69,11 +88,30 @@ OUTPUT
|
||||
INSERTED.model,
|
||||
INSERTED.created_at,
|
||||
INSERTED.updated_at,
|
||||
INSERTED.state
|
||||
INSERTED.state,
|
||||
INSERTED.started_by
|
||||
WHERE id = @id AND companyuser_id = @companyuserId
|
||||
`;
|
||||
|
||||
/**
|
||||
* Deletes a conversation and, because started_by is not a foreign key, the whole chain of
|
||||
* subagent conversations below it (started_by -> id, recursively) in a single statement.
|
||||
* Messages go with their conversations via ON DELETE CASCADE. rowsAffected is 0 only when
|
||||
* the root conversation does not exist for this user (a root always yields >= 1 row).
|
||||
*/
|
||||
export const DELETE_BY_COMPANYUSER_AND_ID = `
|
||||
WITH subtree AS (
|
||||
SELECT id
|
||||
FROM modelconversation
|
||||
WHERE id = @id AND companyuser_id = @companyuserId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id
|
||||
FROM modelconversation c
|
||||
INNER JOIN subtree s ON c.started_by = s.id
|
||||
WHERE c.companyuser_id = @companyuserId
|
||||
)
|
||||
DELETE FROM modelconversation
|
||||
WHERE id = @id AND companyuser_id = @companyuserId
|
||||
WHERE id IN (SELECT id FROM subtree)
|
||||
`;
|
||||
|
||||
@@ -5,8 +5,8 @@ IF NOT EXISTS (
|
||||
WHERE company_id = @companyId AND [type] = @type
|
||||
)
|
||||
BEGIN
|
||||
INSERT INTO numbers (company_id, [type])
|
||||
VALUES (@companyId, @type);
|
||||
INSERT INTO numbers (company_id, [type], prefix)
|
||||
VALUES (@companyId, @type, ISNULL(@prefix, ''));
|
||||
END
|
||||
`;
|
||||
|
||||
|
||||
57
src/providers/mssql/queries/shipment.js
Normal file
57
src/providers/mssql/queries/shipment.js
Normal file
@@ -0,0 +1,57 @@
|
||||
export const SHIPMENT_TYPE = 'shipment';
|
||||
|
||||
export const GET_ORDER_HEADER = `
|
||||
SELECT id, company_id AS companyId, [number], [type], entity_id AS entityId, external_number AS externalNumber, [date]
|
||||
FROM manifest
|
||||
WHERE id = @orderId AND company_id = @companyId AND [type] = 'order';
|
||||
`;
|
||||
|
||||
export const GET_ORDER_LINES = `
|
||||
SELECT id, sku, name, pu, amount, net, rebate, taxrate, comment
|
||||
FROM manifestarticle
|
||||
WHERE manifest_id = @orderId
|
||||
ORDER BY id ASC;
|
||||
`;
|
||||
|
||||
export const GET_SHIPMENT_PREFIX = `
|
||||
SELECT prefix
|
||||
FROM numbers
|
||||
WHERE company_id = @companyId AND [type] = 'shipment';
|
||||
`;
|
||||
|
||||
export const LIST_SHIPMENTS_BY_ORDER_NUMBER_PATTERN = `
|
||||
SELECT m.id, m.[number], m.[type], m.entity_id AS entityId, m.external_number AS externalNumber, m.[date], m.created_at AS createdAt
|
||||
FROM manifest m
|
||||
WHERE m.company_id = @companyId
|
||||
AND m.[type] = 'shipment'
|
||||
AND m.[number] LIKE @pattern
|
||||
ORDER BY m.id ASC;
|
||||
`;
|
||||
|
||||
export const GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN = `
|
||||
SELECT ma.sku, SUM(ma.amount) AS shippedAmount
|
||||
FROM manifest m
|
||||
INNER JOIN manifestarticle ma ON ma.manifest_id = m.id
|
||||
WHERE m.company_id = @companyId
|
||||
AND m.[type] = 'shipment'
|
||||
AND m.[number] LIKE @pattern
|
||||
AND (@excludeShipmentId IS NULL OR m.id != @excludeShipmentId)
|
||||
GROUP BY ma.sku;
|
||||
`;
|
||||
|
||||
export const GET_SHIPMENT_HEADER = `
|
||||
SELECT m.id, m.[number], m.[type], m.entity_id AS entityId, m.external_number AS externalNumber, m.[date], m.created_at AS createdAt
|
||||
FROM manifest m
|
||||
WHERE m.id = @id AND m.company_id = @companyId AND m.[type] = 'shipment';
|
||||
`;
|
||||
|
||||
export const CHECK_SHIPMENT_PACKAGES_SHIPPED = `
|
||||
SELECT TOP 1 id
|
||||
FROM shipping
|
||||
WHERE manifest_id = @id AND LOWER([state]) = 'shipped';
|
||||
`;
|
||||
|
||||
export const DELETE_SHIPMENT = `
|
||||
DELETE FROM manifest
|
||||
WHERE id = @id AND company_id = @companyId AND [type] = 'shipment';
|
||||
`;
|
||||
@@ -372,6 +372,7 @@ BEGIN
|
||||
companyuser_id INT NULL,
|
||||
[date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(),
|
||||
[text] NVARCHAR(MAX) NOT NULL,
|
||||
meta NVARCHAR(MAX) NULL,
|
||||
CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_articlehistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||
);
|
||||
@@ -818,10 +819,12 @@ BEGIN
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_created_at DEFAULT SYSUTCDATETIME(),
|
||||
updated_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_updated_at DEFAULT SYSUTCDATETIME(),
|
||||
state NVARCHAR(20) NOT NULL CONSTRAINT DF_modelconversation_state DEFAULT 'idle',
|
||||
started_by INT NULL, -- id of the conversation that started this one as a subagent; deliberately NOT a foreign key
|
||||
CONSTRAINT FK_modelconversation_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE,
|
||||
CONSTRAINT CK_modelconversation_state CHECK (state IN ('idle', 'busy'))
|
||||
);
|
||||
CREATE INDEX IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at);
|
||||
CREATE INDEX IX_modelconversation_started_by ON modelconversation (started_by);
|
||||
END
|
||||
`;
|
||||
|
||||
@@ -1148,6 +1151,8 @@ BEGIN
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
manifest_id INT NOT NULL,
|
||||
[number] NVARCHAR(100) NOT NULL,
|
||||
tracking_code NVARCHAR(255) NULL,
|
||||
carrier NVARCHAR(100) NULL,
|
||||
[type] NVARCHAR(100) NOT NULL,
|
||||
[state] NVARCHAR(100) NOT NULL,
|
||||
CONSTRAINT FK_shipping_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
|
||||
@@ -1172,6 +1177,99 @@ BEGIN
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_ORDERSUMMARY = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ordersummary')
|
||||
BEGIN
|
||||
CREATE TABLE ordersummary (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
company_id INT NOT NULL,
|
||||
manifest_id INT NOT NULL CONSTRAINT UQ_ordersummary_manifest UNIQUE,
|
||||
order_number NVARCHAR(100) NOT NULL,
|
||||
order_date DATETIME2 NOT NULL,
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_ordersummary_created_at DEFAULT SYSUTCDATETIME(),
|
||||
entity_id INT NULL,
|
||||
customer_name NVARCHAR(255) NULL,
|
||||
positions INT NOT NULL CONSTRAINT DF_ordersummary_positions DEFAULT 0,
|
||||
gross_total FLOAT NOT NULL CONSTRAINT DF_ordersummary_gross_total DEFAULT 0,
|
||||
paid_total FLOAT NOT NULL CONSTRAINT DF_ordersummary_paid_total DEFAULT 0,
|
||||
ordered_quantity FLOAT NOT NULL CONSTRAINT DF_ordersummary_ordered_quantity DEFAULT 0,
|
||||
shipped_quantity FLOAT NOT NULL CONSTRAINT DF_ordersummary_shipped_quantity DEFAULT 0,
|
||||
is_paid TINYINT NOT NULL CONSTRAINT DF_ordersummary_is_paid DEFAULT 0,
|
||||
is_shipped TINYINT NOT NULL CONSTRAINT DF_ordersummary_is_shipped DEFAULT 0,
|
||||
CONSTRAINT FK_ordersummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||
CONSTRAINT FK_ordersummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_ordersummary_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IX_ordersummary_company_date
|
||||
ON ordersummary (company_id, order_date DESC, manifest_id DESC);
|
||||
|
||||
CREATE INDEX IX_ordersummary_company_shipped
|
||||
ON ordersummary (company_id, is_shipped);
|
||||
|
||||
CREATE INDEX IX_ordersummary_company_paid
|
||||
ON ordersummary (company_id, is_paid);
|
||||
|
||||
CREATE INDEX IX_ordersummary_company_number
|
||||
ON ordersummary (company_id, order_number);
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_MCP_CLIENT = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client')
|
||||
BEGIN
|
||||
CREATE TABLE mcp_client (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
client_id NVARCHAR(100) NOT NULL,
|
||||
client_secret NVARCHAR(255) NULL,
|
||||
client_name NVARCHAR(255) NOT NULL,
|
||||
redirect_uris NVARCHAR(MAX) NOT NULL,
|
||||
grant_types NVARCHAR(255) NULL,
|
||||
response_types NVARCHAR(255) NULL,
|
||||
scope NVARCHAR(255) NULL,
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_client_created_at DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_mcp_client_client_id UNIQUE (client_id)
|
||||
);
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_MCP_AUTH_CODE = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_auth_code')
|
||||
BEGIN
|
||||
CREATE TABLE mcp_auth_code (
|
||||
code NVARCHAR(128) PRIMARY KEY,
|
||||
client_id NVARCHAR(100) NOT NULL,
|
||||
google_auth_id INT NOT NULL,
|
||||
redirect_uri NVARCHAR(2048) NOT NULL,
|
||||
code_challenge NVARCHAR(255) NOT NULL,
|
||||
code_challenge_method NVARCHAR(32) NOT NULL,
|
||||
scope NVARCHAR(255) NULL,
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
used TINYINT NOT NULL CONSTRAINT DF_mcp_auth_code_used DEFAULT 0,
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_auth_code_created_at DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_mcp_auth_code_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE
|
||||
);
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_MCP_TOKEN = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_token')
|
||||
BEGIN
|
||||
CREATE TABLE mcp_token (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
refresh_token NVARCHAR(128) NOT NULL,
|
||||
client_id NVARCHAR(100) NOT NULL,
|
||||
google_auth_id INT NOT NULL,
|
||||
scope NVARCHAR(255) NULL,
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
revoked TINYINT NOT NULL CONSTRAINT DF_mcp_token_revoked DEFAULT 0,
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_mcp_token_created_at DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_mcp_token_refresh UNIQUE (refresh_token),
|
||||
CONSTRAINT FK_mcp_token_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE
|
||||
);
|
||||
END
|
||||
`;
|
||||
|
||||
/** DDL scripts in dependency order for schema init. */
|
||||
export const SCHEMA_SCRIPTS = [
|
||||
INIT_META,
|
||||
@@ -1239,4 +1337,8 @@ export const SCHEMA_SCRIPTS = [
|
||||
INIT_PAYMENT,
|
||||
INIT_SHIPPING,
|
||||
INIT_PACKAGE,
|
||||
INIT_ORDERSUMMARY,
|
||||
INIT_MCP_CLIENT,
|
||||
INIT_MCP_AUTH_CODE,
|
||||
INIT_MCP_TOKEN,
|
||||
];
|
||||
|
||||
@@ -46,3 +46,22 @@ export function isForeignKeyConstraintError(err) {
|
||||
err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY'
|
||||
);
|
||||
}
|
||||
|
||||
export const SLOW_QUERY_THRESHOLD_MS = 20;
|
||||
|
||||
/**
|
||||
* Logs a SQL statement to the console if execution time exceeded SLOW_QUERY_THRESHOLD_MS (20ms).
|
||||
*
|
||||
* @param {string} sql
|
||||
* @param {number} durationMs
|
||||
* @param {object} [params]
|
||||
*/
|
||||
export function logSlowQuery(sql, durationMs, params = null) {
|
||||
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
||||
const timeStr = `${durationMs.toFixed(1)}ms`;
|
||||
console.log(`[SQL >20ms] (${timeStr})\n${typeof sql === 'string' ? sql.trim() : sql}`);
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
console.log('Params:', params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { logSlowQuery } from '../provider.js';
|
||||
import { SCHEMA_SCRIPTS } from './schema.js';
|
||||
|
||||
function normalizeParamValue(value) {
|
||||
@@ -19,13 +20,23 @@ function normalizeParamValue(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function bindParams(params) {
|
||||
return Object.fromEntries(
|
||||
function bindParams(params, sql = null) {
|
||||
const bound = Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [
|
||||
key.replace(/^[@:$]/, ''),
|
||||
normalizeParamValue(value),
|
||||
]),
|
||||
);
|
||||
if (typeof sql === 'string') {
|
||||
const matches = sql.matchAll(/[@:$]([a-zA-Z0-9_]+)/g);
|
||||
for (const match of matches) {
|
||||
const name = match[1];
|
||||
if (!(name in bound)) {
|
||||
bound[name] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
function normalizeInsertId(value) {
|
||||
@@ -226,24 +237,29 @@ export class BetterSqlite3Executor {
|
||||
|
||||
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
||||
async query(sql, params = {}) {
|
||||
const statement = this.db.prepare(sql);
|
||||
const bindings = bindParams(params);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const statement = this.db.prepare(sql);
|
||||
const bindings = bindParams(params, sql);
|
||||
|
||||
if (statement.reader) {
|
||||
const rows = statement.all(bindings);
|
||||
if (statement.reader) {
|
||||
const rows = statement.all(bindings);
|
||||
return {
|
||||
rows,
|
||||
rowsAffected: rows.length,
|
||||
insertId: normalizeInsertId(rows[0]?.id),
|
||||
};
|
||||
}
|
||||
|
||||
const result = statement.run(bindings);
|
||||
return {
|
||||
rows,
|
||||
rowsAffected: rows.length,
|
||||
insertId: normalizeInsertId(rows[0]?.id),
|
||||
rows: [],
|
||||
rowsAffected: result.changes,
|
||||
insertId: normalizeInsertId(result.lastInsertRowid),
|
||||
};
|
||||
} finally {
|
||||
logSlowQuery(sql, performance.now() - start, params);
|
||||
}
|
||||
|
||||
const result = statement.run(bindings);
|
||||
return {
|
||||
rows: [],
|
||||
rowsAffected: result.changes,
|
||||
insertId: normalizeInsertId(result.lastInsertRowid),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const queries = translateQueries(source, 'articlehistory');
|
||||
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
|
||||
|
||||
export const LIST_BY_ARTICLE_ID_LIMITED = `
|
||||
SELECT id, companyuser_id AS companyuserId, date, text
|
||||
SELECT id, companyuser_id AS companyuserId, date, text, meta
|
||||
FROM articlehistory
|
||||
WHERE article_id = @articleId
|
||||
ORDER BY date DESC, id DESC
|
||||
|
||||
@@ -79,7 +79,13 @@ WITH RECURSIVE tree AS (
|
||||
SELECT
|
||||
t.id, t.parent_id AS parentId, t.sort, ct.name, ct.slug,
|
||||
p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH,
|
||||
p.thumbhash AS thumbHash
|
||||
p.thumbhash AS thumbHash,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM categoryarticle ca
|
||||
INNER JOIN article a ON a.id = ca.article_id
|
||||
WHERE ca.category_id = t.id AND a.parentarticle_id IS NULL
|
||||
) AS articleCount
|
||||
FROM tree t
|
||||
LEFT JOIN categorytext ct
|
||||
ON ct.category_id = t.id AND ct.language_id = @languageId
|
||||
@@ -89,4 +95,17 @@ LEFT JOIN pic p ON p.id = (
|
||||
ORDER BY cp.sort, cp.id LIMIT 1
|
||||
)
|
||||
ORDER BY t.sort, t.id
|
||||
`;
|
||||
|
||||
export const COUNT_ARTICLES_IN_TREE = `
|
||||
WITH RECURSIVE tree AS (
|
||||
SELECT id FROM category WHERE id = @rootId
|
||||
UNION ALL
|
||||
SELECT c.id FROM category c INNER JOIN tree t ON c.parent_id = t.id
|
||||
)
|
||||
SELECT COUNT(DISTINCT ca.article_id) AS total
|
||||
FROM categoryarticle ca
|
||||
INNER JOIN article a ON a.id = ca.article_id
|
||||
WHERE ca.category_id IN (SELECT id FROM tree)
|
||||
AND a.parentarticle_id IS NULL
|
||||
`;
|
||||
@@ -38,8 +38,8 @@ WHERE sku = @sku
|
||||
`;
|
||||
|
||||
export const INSERT_ARTICLE_HISTORY = `
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, text)
|
||||
VALUES (@articleId, @companyuserId, @text);
|
||||
INSERT INTO articlehistory (article_id, companyuser_id, date, text, meta)
|
||||
VALUES (@articleId, @companyuserId, COALESCE(@date, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), @text, @meta);
|
||||
`;
|
||||
|
||||
export const GET_LATEST_ROW = `
|
||||
|
||||
18
src/providers/sqlite3/queries/mcp.js
Normal file
18
src/providers/sqlite3/queries/mcp.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as source from '../../mssql/queries/mcp.js';
|
||||
import { translateQueries } from './translate.js';
|
||||
|
||||
const queries = translateQueries(source, 'mcp');
|
||||
export const {
|
||||
INSERT_CLIENT,
|
||||
GET_CLIENT,
|
||||
LIST_CLIENTS,
|
||||
DELETE_CLIENT,
|
||||
INSERT_AUTH_CODE,
|
||||
GET_AUTH_CODE,
|
||||
CONSUME_AUTH_CODE,
|
||||
INSERT_REFRESH_TOKEN,
|
||||
GET_REFRESH_TOKEN,
|
||||
REVOKE_REFRESH_TOKEN,
|
||||
LIST_AUTHORIZATIONS_BY_USER,
|
||||
REVOKE_USER_AUTHORIZATIONS,
|
||||
} = queries;
|
||||
@@ -5,8 +5,8 @@ const queries = translateQueries(source, 'numbers');
|
||||
export const { GET_BY_COMPANY_AND_ID, LIST_BY_COMPANY, UPDATE_BY_COMPANY_AND_ID } = queries;
|
||||
|
||||
export const ENSURE_TYPE = `
|
||||
INSERT INTO numbers (company_id, "type")
|
||||
VALUES (@companyId, @type)
|
||||
INSERT INTO numbers (company_id, "type", prefix)
|
||||
VALUES (@companyId, @type, COALESCE(@prefix, ''))
|
||||
ON CONFLICT (company_id, "type") DO NOTHING
|
||||
`;
|
||||
|
||||
|
||||
15
src/providers/sqlite3/queries/shipment.js
Normal file
15
src/providers/sqlite3/queries/shipment.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as source from '../../mssql/queries/shipment.js';
|
||||
import { translateQueries } from './translate.js';
|
||||
|
||||
const queries = translateQueries(source, 'shipment');
|
||||
export const {
|
||||
SHIPMENT_TYPE,
|
||||
GET_ORDER_HEADER,
|
||||
GET_ORDER_LINES,
|
||||
GET_SHIPMENT_PREFIX,
|
||||
LIST_SHIPMENTS_BY_ORDER_NUMBER_PATTERN,
|
||||
GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN,
|
||||
GET_SHIPMENT_HEADER,
|
||||
CHECK_SHIPMENT_PACKAGES_SHIPPED,
|
||||
DELETE_SHIPMENT,
|
||||
} = queries;
|
||||
@@ -511,9 +511,11 @@ CREATE TABLE IF NOT EXISTS modelconversation (
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
state TEXT NOT NULL DEFAULT 'idle' CHECK (state IN ('idle', 'busy')),
|
||||
started_by INTEGER, -- id of the conversation that started this one as a subagent; deliberately NOT a foreign key
|
||||
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at);
|
||||
CREATE INDEX IF NOT EXISTS IX_modelconversation_started_by ON modelconversation (started_by);
|
||||
`;
|
||||
|
||||
|
||||
@@ -551,6 +553,7 @@ CREATE TABLE IF NOT EXISTS articlehistory (
|
||||
companyuser_id INTEGER,
|
||||
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
"text" TEXT NOT NULL,
|
||||
meta TEXT,
|
||||
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||
);
|
||||
@@ -820,6 +823,8 @@ CREATE TABLE IF NOT EXISTS shipping (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
manifest_id INTEGER NOT NULL,
|
||||
"number" TEXT NOT NULL,
|
||||
tracking_code TEXT NULL,
|
||||
carrier TEXT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
|
||||
@@ -840,6 +845,81 @@ CREATE TABLE IF NOT EXISTS package (
|
||||
);
|
||||
`;
|
||||
|
||||
export const INIT_ORDERSUMMARY = `
|
||||
CREATE TABLE IF NOT EXISTS ordersummary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
company_id INTEGER NOT NULL,
|
||||
manifest_id INTEGER NOT NULL UNIQUE,
|
||||
order_number TEXT NOT NULL,
|
||||
order_date TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
entity_id INTEGER NULL,
|
||||
customer_name TEXT NULL,
|
||||
positions INTEGER NOT NULL DEFAULT 0,
|
||||
gross_total REAL NOT NULL DEFAULT 0,
|
||||
paid_total REAL NOT NULL DEFAULT 0,
|
||||
ordered_quantity REAL NOT NULL DEFAULT 0,
|
||||
shipped_quantity REAL NOT NULL DEFAULT 0,
|
||||
is_paid INTEGER NOT NULL DEFAULT 0,
|
||||
is_shipped INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS IX_ordersummary_company_date
|
||||
ON ordersummary (company_id, order_date DESC, manifest_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS IX_ordersummary_company_shipped
|
||||
ON ordersummary (company_id, is_shipped);
|
||||
CREATE INDEX IF NOT EXISTS IX_ordersummary_company_paid
|
||||
ON ordersummary (company_id, is_paid);
|
||||
CREATE INDEX IF NOT EXISTS IX_ordersummary_company_number
|
||||
ON ordersummary (company_id, order_number);
|
||||
`;
|
||||
|
||||
export const INIT_MCP_CLIENT = `
|
||||
CREATE TABLE IF NOT EXISTS mcp_client (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id TEXT NOT NULL UNIQUE,
|
||||
client_secret TEXT,
|
||||
client_name TEXT NOT NULL,
|
||||
redirect_uris TEXT NOT NULL,
|
||||
grant_types TEXT,
|
||||
response_types TEXT,
|
||||
scope TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW})
|
||||
);
|
||||
`;
|
||||
|
||||
export const INIT_MCP_AUTH_CODE = `
|
||||
CREATE TABLE IF NOT EXISTS mcp_auth_code (
|
||||
code TEXT PRIMARY KEY,
|
||||
client_id TEXT NOT NULL,
|
||||
google_auth_id INTEGER NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
code_challenge TEXT NOT NULL,
|
||||
code_challenge_method TEXT NOT NULL,
|
||||
scope TEXT,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
export const INIT_MCP_TOKEN = `
|
||||
CREATE TABLE IF NOT EXISTS mcp_token (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
refresh_token TEXT NOT NULL UNIQUE,
|
||||
client_id TEXT NOT NULL,
|
||||
google_auth_id INTEGER NOT NULL,
|
||||
scope TEXT,
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
/** DDL scripts in dependency order for SQLite schema initialization. */
|
||||
export const SCHEMA_SCRIPTS = [
|
||||
INIT_META,
|
||||
@@ -904,4 +984,8 @@ export const SCHEMA_SCRIPTS = [
|
||||
INIT_PAYMENT,
|
||||
INIT_SHIPPING,
|
||||
INIT_PACKAGE,
|
||||
INIT_ORDERSUMMARY,
|
||||
INIT_MCP_CLIENT,
|
||||
INIT_MCP_AUTH_CODE,
|
||||
INIT_MCP_TOKEN,
|
||||
];
|
||||
|
||||
@@ -10,12 +10,14 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
|
||||
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import getBySku from './getBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
import {
|
||||
recordVariantAddedOnParentHistory,
|
||||
recordChildAssignedToParentHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import attachChild from './attachChild.js';
|
||||
|
||||
export default function addChild(provider) {
|
||||
@@ -31,7 +33,6 @@ export default function addChild(provider) {
|
||||
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
|
||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Adds a child article to a parent article (creates and attaches with auto-generated SKU).
|
||||
@@ -242,17 +243,21 @@ export default function addChild(provider) {
|
||||
});
|
||||
|
||||
// History on child
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
await recordChildAssignedToParentHistory(runner, {
|
||||
childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" angelegt`,
|
||||
childName,
|
||||
childSku,
|
||||
parentSku: parent.sku,
|
||||
created: true,
|
||||
});
|
||||
|
||||
// History on parent
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
await recordVariantAddedOnParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||
childName,
|
||||
childSku,
|
||||
});
|
||||
|
||||
return load(googleAuthId, childSku, runner);
|
||||
|
||||
@@ -6,11 +6,13 @@ import * as mssqlArticleAttrVal from '../../providers/mssql/queries/articleattri
|
||||
import * as sqliteArticleAttrVal from '../../providers/sqlite3/queries/articleattributevalue.js';
|
||||
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
||||
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import getBySku from './getBySku.js';
|
||||
import listChildren from './listChildren.js';
|
||||
import {
|
||||
recordChildAssignedToParentHistory,
|
||||
recordVariantAddedOnParentHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function attachChild(provider) {
|
||||
@@ -23,7 +25,6 @@ export default function attachChild(provider) {
|
||||
const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal;
|
||||
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
|
||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Attaches an existing article as a child to a parent article.
|
||||
@@ -288,16 +289,19 @@ export default function attachChild(provider) {
|
||||
|
||||
// History only if not already child
|
||||
if (!isAlreadyChildOfThisParent) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: child.id,
|
||||
await recordChildAssignedToParentHistory(runner, {
|
||||
childId: child.id,
|
||||
companyuserId,
|
||||
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" zugewiesen`,
|
||||
childName,
|
||||
childSku,
|
||||
parentSku: parent.sku,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
await recordVariantAddedOnParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||
childName,
|
||||
childSku,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
|
||||
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||
import nextSku from '../numbers/nextSku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
import { recordArticleCreatedHistory } from '../articlehistory/record.js';
|
||||
|
||||
export default function createUnderParent(provider) {
|
||||
const access = resolveCategoryAccess(provider);
|
||||
@@ -23,7 +22,6 @@ export default function createUnderParent(provider) {
|
||||
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Creates a new article under a given parent category, allocating a SKU and
|
||||
@@ -95,10 +93,10 @@ export default function createUnderParent(provider) {
|
||||
seo: finalSeo,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleCreatedHistory(runner, {
|
||||
articleId: id,
|
||||
companyuserId,
|
||||
text: `Artikel "${name}" angelegt`,
|
||||
name,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,11 +17,11 @@ export default function getHistoryBySku(provider) {
|
||||
* @param {object} [opts] - Optional options.
|
||||
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>} The article history entries.
|
||||
*
|
||||
* @example
|
||||
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||
* // => [{ id, companyuserId, date, text }]
|
||||
* // => [{ id, companyuserId, date, text, meta }]
|
||||
*
|
||||
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
||||
* // => up to 5 most recent entries
|
||||
@@ -51,6 +51,7 @@ export default function getHistoryBySku(provider) {
|
||||
companyuserId: row.companyuserId ?? null,
|
||||
date: row.date,
|
||||
text: row.text,
|
||||
meta: row.meta ?? null,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
|
||||
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||
import {
|
||||
recordParentConvertedHistory,
|
||||
recordVariantAddedOnParentHistory,
|
||||
recordChildAssignedToParentHistory,
|
||||
recordArticleImportedHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import {
|
||||
CREATE as MSSQL_CREATE_BRAND,
|
||||
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME,
|
||||
@@ -55,7 +59,6 @@ export default function importRows(provider) {
|
||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||
const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME;
|
||||
const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND;
|
||||
@@ -210,10 +213,10 @@ export default function importRows(provider) {
|
||||
seo: null,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleImportedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Artikel "${name}" importiert`,
|
||||
name,
|
||||
});
|
||||
|
||||
// Brand (upsert by name).
|
||||
@@ -318,22 +321,24 @@ export default function importRows(provider) {
|
||||
});
|
||||
|
||||
if (!wasParent) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordParentConvertedHistory(runner, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text: 'Artikel zu Vaterartikel konvertiert',
|
||||
});
|
||||
}
|
||||
const childName = String(row?.name ?? sku).trim();
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
await recordVariantAddedOnParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||
childName,
|
||||
childSku: sku,
|
||||
});
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
await recordChildAssignedToParentHistory(runner, {
|
||||
childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childName}" (${sku}) als Variante von "${parentSku}" zugewiesen`,
|
||||
childName,
|
||||
childSku: sku,
|
||||
parentSku,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import articleDelete from './delete.js';
|
||||
import {
|
||||
recordChildUnlinkedFromParentHistory,
|
||||
recordChildDeletedFromParentHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function removeChild(provider) {
|
||||
@@ -12,7 +14,6 @@ export default function removeChild(provider) {
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Removes a child article from a parent article, either by unlinking or deleting it.
|
||||
@@ -78,10 +79,11 @@ export default function removeChild(provider) {
|
||||
await recompactParentSort();
|
||||
|
||||
// History on parent (child article is gone so we can only log on parent)
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
await recordChildDeletedFromParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) gelöscht`,
|
||||
childName,
|
||||
childSku,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -119,18 +121,14 @@ export default function removeChild(provider) {
|
||||
|
||||
await recompactParentSort();
|
||||
|
||||
// Add history on child
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
// Add history on child and parent
|
||||
await recordChildUnlinkedFromParentHistory(runner, {
|
||||
childId,
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
|
||||
});
|
||||
|
||||
// Add history on parent
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) getrennt`,
|
||||
childSku,
|
||||
childName,
|
||||
parentSku,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,10 +6,9 @@ import {
|
||||
GET_BRAND_BY_COMPANY_AND_ID as SQLITE_GET_BRAND,
|
||||
UPDATE_BRAND as SQLITE_UPDATE_BRAND,
|
||||
} from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordBrandChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setBrandIdBySku(provider) {
|
||||
@@ -19,7 +18,6 @@ export default function setBrandIdBySku(provider) {
|
||||
|
||||
const getBrandSql = isSqlite ? SQLITE_GET_BRAND : MSSQL_GET_BRAND;
|
||||
const updateBrandSql = isSqlite ? SQLITE_UPDATE_BRAND : MSSQL_UPDATE_BRAND;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Sets (or clears) the brand of an article by its SKU.
|
||||
@@ -82,13 +80,12 @@ export default function setBrandIdBySku(provider) {
|
||||
}
|
||||
|
||||
// null = no brand; treat as distinct sentinel so first assignment is logged
|
||||
if (oldBrandName !== newBrandName) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Marke ge\u00e4ndert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`,
|
||||
});
|
||||
}
|
||||
await recordBrandChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldBrandName,
|
||||
newBrandName,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
ADD_CATEGORY as SQLITE_ADD_CATEGORY,
|
||||
DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES,
|
||||
} from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordCategoryChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setCategoryIdsBySku(provider) {
|
||||
@@ -21,7 +20,6 @@ export default function setCategoryIdsBySku(provider) {
|
||||
|
||||
const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY;
|
||||
const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Replaces the category membership of an article by its SKU.
|
||||
@@ -86,15 +84,12 @@ export default function setCategoryIdsBySku(provider) {
|
||||
}
|
||||
|
||||
// Write history if categories changed
|
||||
const oldStr = oldCategoryIds.join(', ');
|
||||
const newStr = [...normalized].sort((a, b) => a - b).join(', ');
|
||||
if (oldStr !== newStr) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Kategorien geändert von [${oldStr}] auf [${newStr}]`,
|
||||
});
|
||||
}
|
||||
await recordCategoryChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldCategoryIds,
|
||||
newCategoryIds: normalized,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
|
||||
EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT,
|
||||
} from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordEanChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setEansBySku(provider) {
|
||||
@@ -22,7 +21,6 @@ export default function setEansBySku(provider) {
|
||||
|
||||
const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN;
|
||||
const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT;
|
||||
|
||||
/**
|
||||
@@ -93,15 +91,12 @@ export default function setEansBySku(provider) {
|
||||
}
|
||||
|
||||
// Write history if EANs changed
|
||||
const oldStr = oldEans.join(', ');
|
||||
const newStr = [...normalized].sort().join(', ');
|
||||
if (oldStr !== newStr) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `EANs geändert von "${oldStr}" auf "${newStr}"`,
|
||||
});
|
||||
}
|
||||
await recordEanChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldEans,
|
||||
newEans: normalized,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -2,10 +2,13 @@ import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import {
|
||||
recordParentConvertedHistory,
|
||||
recordVariantAddedOnParentHistory,
|
||||
recordChildAssignedToParentHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setParentBySku(provider) {
|
||||
@@ -15,7 +18,6 @@ export default function setParentBySku(provider) {
|
||||
|
||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Links an article to a parent article (V/K compound) by their SKUs.
|
||||
@@ -118,23 +120,25 @@ export default function setParentBySku(provider) {
|
||||
});
|
||||
|
||||
if (!wasParent) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordParentConvertedHistory(runner, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text: 'Artikel zu Vaterartikel konvertiert',
|
||||
});
|
||||
}
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: parentId,
|
||||
await recordVariantAddedOnParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||
childName,
|
||||
childSku: sku,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
await recordChildAssignedToParentHistory(runner, {
|
||||
childId: articleId,
|
||||
companyuserId,
|
||||
text: `Artikel "${sku}" als Variante von "${parentSku}" zugewiesen`,
|
||||
childName: null,
|
||||
childSku: sku,
|
||||
parentSku,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
CREATE as SQLITE_CREATE_PRICE,
|
||||
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
|
||||
} from '../../providers/sqlite3/queries/price.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordPriceChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
function normalizePrices(prices) {
|
||||
@@ -66,7 +65,6 @@ export default function setPricesBySku(provider) {
|
||||
|
||||
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
|
||||
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Replaces the price list of an article by its SKU.
|
||||
@@ -136,16 +134,12 @@ export default function setPricesBySku(provider) {
|
||||
}
|
||||
|
||||
// Write history if prices changed
|
||||
const fmtPrice = (p) => `${p.net}@${p.taxRate ?? 0}%`;
|
||||
const oldStr = oldPrices.map(fmtPrice).join(', ');
|
||||
const newStr = normalized.map((e) => `${e.price}@${e.tax}%`).join(', ');
|
||||
if (oldStr !== newStr) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Preise geändert von [${oldStr}] auf [${newStr}]`,
|
||||
});
|
||||
}
|
||||
await recordPriceChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldPrices,
|
||||
newPrices: normalized,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { UPDATE_TRACK_STOCK as MSSQL_UPDATE_TRACK_STOCK } from '../../providers/mssql/queries/article.js';
|
||||
import { UPDATE_TRACK_STOCK as SQLITE_UPDATE_TRACK_STOCK } from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordTrackStockChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setTrackStockBySku(provider) {
|
||||
@@ -13,7 +12,6 @@ export default function setTrackStockBySku(provider) {
|
||||
provider?.dialect === 'sqlite3'
|
||||
? SQLITE_UPDATE_TRACK_STOCK
|
||||
: MSSQL_UPDATE_TRACK_STOCK;
|
||||
const insertHistorySql = provider?.dialect === 'sqlite3' ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Sets the track_stock flag of an article by its SKU.
|
||||
@@ -55,13 +53,12 @@ export default function setTrackStockBySku(provider) {
|
||||
}
|
||||
|
||||
// Write history if track_stock changed
|
||||
if (oldTrack !== newTrack) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Bestandsführung geändert von "${oldTrack ? 'Ja' : 'Nein'}" auf "${newTrack ? 'Ja' : 'Nein'}"`,
|
||||
});
|
||||
}
|
||||
await recordTrackStockChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldTrack,
|
||||
newTrack,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { UPDATE_WEIGHT as MSSQL_UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
|
||||
import { UPDATE_WEIGHT as SQLITE_UPDATE_WEIGHT } from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordWeightChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setWeightBySku(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const load = queryBySku(provider);
|
||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE_WEIGHT : MSSQL_UPDATE_WEIGHT;
|
||||
const insertHistorySql = provider?.dialect === 'sqlite3' ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Sets the weight of an article by its SKU.
|
||||
@@ -51,13 +49,12 @@ export default function setWeightBySku(provider) {
|
||||
}
|
||||
|
||||
// Write history if weight changed
|
||||
if (Number(oldWeight) !== weight) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Gewicht geändert von "${oldWeight}" auf "${weight}"`,
|
||||
});
|
||||
}
|
||||
await recordWeightChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldWeight,
|
||||
newWeight: weight,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
|
||||
@@ -4,10 +4,12 @@ import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/
|
||||
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||
import * as mssqlAttr from '../../providers/mssql/queries/attributename.js';
|
||||
import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import getBySku from './getBySku.js';
|
||||
import {
|
||||
recordParentConvertedHistory,
|
||||
recordParentDeactivatedHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function toggleParent(provider) {
|
||||
@@ -18,7 +20,6 @@ export default function toggleParent(provider) {
|
||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||
const attrQueries = isSqlite ? sqliteAttr : mssqlAttr;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Toggles an article into or out of parent mode.
|
||||
@@ -142,32 +143,11 @@ export default function toggleParent(provider) {
|
||||
});
|
||||
|
||||
// Add history entry recording parent conversion and deleted prices
|
||||
const historyLines = [
|
||||
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
|
||||
];
|
||||
|
||||
if (existingPrices.length > 0) {
|
||||
const priceParts = existingPrices.map((p) => {
|
||||
let str = `${p.net} €`;
|
||||
if (p.taxRate != null) {
|
||||
str += ` (${p.taxRate}% MwSt.)`;
|
||||
}
|
||||
if (p.minmultiplier > 1) {
|
||||
str += ` ab ${p.minmultiplier} Stk.`;
|
||||
}
|
||||
if (p.customerGroupName) {
|
||||
str += ` [${p.customerGroupName}]`;
|
||||
}
|
||||
return str;
|
||||
});
|
||||
const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht';
|
||||
historyLines.push(`${label}: ${priceParts.join(', ')}`);
|
||||
}
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordParentConvertedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: historyLines.join('\n'),
|
||||
attributeName,
|
||||
existingPrices,
|
||||
});
|
||||
} else {
|
||||
// Disabling parent: verify zero children
|
||||
@@ -183,10 +163,9 @@ export default function toggleParent(provider) {
|
||||
parentAttributeNameId: null,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordParentDeactivatedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: 'Vaterartikel-Modus deaktiviert',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as MSSQL_UPDATE_TEXT } from '../../providers/mssql/queries/articletext.js';
|
||||
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as SQLITE_UPDATE_TEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||
import { GET_DETAIL_BY_ARTICLE_ID as MSSQL_GET_DETAIL } from '../../providers/mssql/queries/articleDetail.js';
|
||||
@@ -12,6 +10,7 @@ import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
import { recordArticleUpdateHistory } from '../articlehistory/record.js';
|
||||
|
||||
function formatUnitLabel(amount, symbol) {
|
||||
const sym = symbol ?? '';
|
||||
@@ -28,7 +27,6 @@ export default function updateBySku(provider) {
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
const updateTextSql = isSqlite ? SQLITE_UPDATE_TEXT : MSSQL_UPDATE_TEXT;
|
||||
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
|
||||
const getUnitSql = isSqlite ? SQLITE_GET_UNIT : MSSQL_GET_UNIT;
|
||||
@@ -221,10 +219,10 @@ export default function updateBySku(provider) {
|
||||
}
|
||||
|
||||
if (historyLines.length) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleUpdateHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: historyLines.join('\n'),
|
||||
historyLines,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
INSERT_ROW as SQLITE_INSERT,
|
||||
GET_VALUE_DETAIL as SQLITE_GET_DETAIL,
|
||||
} from '../../providers/sqlite3/queries/articleattributevalue.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import { recordAttributeChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setByArticleSku(provider) {
|
||||
@@ -19,7 +18,6 @@ export default function setByArticleSku(provider) {
|
||||
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Replaces all attribute values for an article by its SKU.
|
||||
@@ -182,16 +180,12 @@ export default function setByArticleSku(provider) {
|
||||
}
|
||||
|
||||
// Write history if attribute values changed
|
||||
const fmtAttr = (a) => `${a.attributeName}: ${a.value}`;
|
||||
const oldStr = oldAttrs.map(fmtAttr).join(', ');
|
||||
const newStr = validated.map(fmtAttr).join(', ');
|
||||
if (oldStr !== newStr) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Attributwerte geändert von [${oldStr}] auf [${newStr}]`,
|
||||
});
|
||||
}
|
||||
await recordAttributeChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldAttrs,
|
||||
newAttrs: validated,
|
||||
});
|
||||
|
||||
return inserted;
|
||||
};
|
||||
|
||||
855
src/store/articlehistory/record.js
Normal file
855
src/store/articlehistory/record.js
Normal file
@@ -0,0 +1,855 @@
|
||||
import { INSERT as MSSQL_INSERT } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
|
||||
/**
|
||||
* Formats a numeric currency amount in German language style: "€ 1.000,99"
|
||||
*
|
||||
* @param {number|string} amount - The currency amount.
|
||||
* @returns {string} The formatted currency string.
|
||||
*/
|
||||
export function formatCurrencyDe(amount) {
|
||||
if (amount == null || !Number.isFinite(Number(amount))) return '';
|
||||
const num = Number(amount);
|
||||
const formatted = new Intl.NumberFormat('de-DE', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(num);
|
||||
return `€ ${formatted}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates unit gross price given net, rebate, and taxrate.
|
||||
*
|
||||
* @param {number|string} net - Unit net price.
|
||||
* @param {number|string} [rebate=0] - Rebate percentage.
|
||||
* @param {number|string} [taxrate=0] - Tax rate percentage.
|
||||
* @returns {number} The calculated gross price.
|
||||
*/
|
||||
export function calculateGross(net, rebate = 0, taxrate = 0) {
|
||||
const n = Number(net || 0);
|
||||
const r = Number(rebate || 0);
|
||||
const t = Number(taxrate || 0);
|
||||
return n * (1 - r / 100) * (1 + t / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core function for recording an article history entry.
|
||||
* Serializes meta into a JSON string if provided as an object.
|
||||
*
|
||||
* @param {object} runner - The database runner.
|
||||
* @param {object} params - Insertion parameters.
|
||||
* @param {number} params.articleId - The ID of the article.
|
||||
* @param {number|null} [params.companyuserId] - The ID of the acting company user.
|
||||
* @param {string} params.text - The human-readable history text.
|
||||
* @param {object|string|null} [params.meta] - Structured meta object or JSON string.
|
||||
* @param {string} [params.type] - Optional fallback type if meta is not an object.
|
||||
* @returns {Promise<object>} Query result.
|
||||
*/
|
||||
export async function recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
type,
|
||||
}) {
|
||||
if (!articleId) {
|
||||
throw new Error('articleId required for article history');
|
||||
}
|
||||
const cleanText = typeof text === 'string' ? text.trim() : '';
|
||||
if (!cleanText) {
|
||||
throw new Error('text required for article history');
|
||||
}
|
||||
|
||||
const isSqlite = runner?.dialect === 'sqlite3';
|
||||
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
|
||||
let metaObj = meta;
|
||||
if (metaObj === undefined || metaObj === null) {
|
||||
metaObj = type ? { type } : null;
|
||||
} else if (typeof metaObj === 'object' && type && !metaObj.type) {
|
||||
metaObj = { type, ...metaObj };
|
||||
}
|
||||
|
||||
let serializedMeta = null;
|
||||
if (metaObj !== undefined && metaObj !== null) {
|
||||
serializedMeta = typeof metaObj === 'string' ? metaObj : JSON.stringify(metaObj);
|
||||
}
|
||||
|
||||
let parsedDate = null;
|
||||
if (date != null) {
|
||||
parsedDate = date instanceof Date ? date : new Date(date);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
parsedDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
return runner.query(insertSql, {
|
||||
articleId,
|
||||
companyuserId: companyuserId ?? null,
|
||||
date: parsedDate,
|
||||
text: cleanText,
|
||||
meta: serializedMeta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an order entry in article history, including German currency formatted gross price.
|
||||
* Format: "Auftrag {number} (Kunde: "{customerName}", Menge: {amount}, € 1.000,99)"
|
||||
*/
|
||||
export async function recordOrderHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
orderId,
|
||||
number,
|
||||
customerName,
|
||||
amount,
|
||||
net,
|
||||
rebate,
|
||||
taxrate,
|
||||
}) {
|
||||
const details = [];
|
||||
if (customerName) {
|
||||
details.push(`Kunde: "${customerName}"`);
|
||||
}
|
||||
if (amount != null && Number.isFinite(Number(amount))) {
|
||||
details.push(`Menge: ${amount}`);
|
||||
}
|
||||
let gross = null;
|
||||
if (net != null && Number.isFinite(Number(net))) {
|
||||
gross = calculateGross(net, rebate, taxrate);
|
||||
const formattedGross = formatCurrencyDe(gross);
|
||||
if (formattedGross) {
|
||||
details.push(formattedGross);
|
||||
}
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
const text = `Auftrag ${number}${suffix}`;
|
||||
|
||||
const meta = {
|
||||
type: 'order',
|
||||
...(orderId != null ? { orderId } : {}),
|
||||
orderNumber: number,
|
||||
customerName: customerName ?? null,
|
||||
amount: amount != null ? Number(amount) : null,
|
||||
net: net != null ? Number(net) : null,
|
||||
rebate: rebate != null ? Number(rebate) : 0,
|
||||
taxrate: taxrate != null ? Number(taxrate) : 0,
|
||||
gross: gross != null ? Math.round(gross * 100) / 100 : null,
|
||||
};
|
||||
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a delivery / warehouse ledger entry in article history.
|
||||
*/
|
||||
export async function recordDeliveryHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
manifestNumber,
|
||||
supplierName,
|
||||
warehouseName,
|
||||
net,
|
||||
rebate,
|
||||
taxrate,
|
||||
amount,
|
||||
transactionText,
|
||||
isDelivery = true,
|
||||
}) {
|
||||
let historyText = transactionText;
|
||||
let ekBrutto = null;
|
||||
|
||||
if (isDelivery) {
|
||||
const details = [];
|
||||
if (supplierName) {
|
||||
details.push(`Lieferant: "${supplierName}"`);
|
||||
}
|
||||
if (warehouseName) {
|
||||
details.push(`Lager: "${warehouseName}"`);
|
||||
}
|
||||
if (net != null && Number.isFinite(Number(net))) {
|
||||
const netNum = Number(net);
|
||||
const rebateNum = Number(rebate ?? 0);
|
||||
const taxrateNum = Number(taxrate ?? 0);
|
||||
const netAfterRebate = netNum * (1 - rebateNum / 100);
|
||||
const brutto = netAfterRebate * (1 + taxrateNum / 100);
|
||||
ekBrutto = (Math.round(brutto * 100) / 100).toFixed(2);
|
||||
details.push(`EK brutto: ${ekBrutto}`);
|
||||
}
|
||||
if (amount != null && Number.isFinite(Number(amount))) {
|
||||
details.push(`Menge: ${amount}`);
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
const base =
|
||||
transactionText?.trim() ||
|
||||
(manifestNumber ? `Lieferung ${manifestNumber}` : 'Lieferung');
|
||||
historyText = `${base}${suffix}`;
|
||||
} else if (warehouseName && transactionText && !transactionText.includes(warehouseName)) {
|
||||
historyText = `${transactionText} (Lager: "${warehouseName}")`;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
type: isDelivery ? 'delivery' : 'movement',
|
||||
manifestNumber: manifestNumber ?? null,
|
||||
supplierName: supplierName ?? null,
|
||||
warehouseName: warehouseName ?? null,
|
||||
amount: amount != null ? Number(amount) : null,
|
||||
net: net != null ? Number(net) : null,
|
||||
rebate: rebate != null ? Number(rebate) : 0,
|
||||
taxrate: taxrate != null ? Number(taxrate) : 0,
|
||||
ekBrutto: ekBrutto != null ? Number(ekBrutto) : null,
|
||||
transactionText: transactionText ?? null,
|
||||
};
|
||||
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text: historyText,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a stock adjustment in article history.
|
||||
*/
|
||||
export async function recordStockAdjustmentHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
amount,
|
||||
reason,
|
||||
}) {
|
||||
const sign = amount > 0 ? '+' : '';
|
||||
const text = `Bestandskorrektur ${sign}${amount} (${reason})`;
|
||||
const meta = {
|
||||
type: 'stock_adjustment',
|
||||
amount,
|
||||
reason,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records price changes in article history.
|
||||
*/
|
||||
export async function recordPriceChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldPrices,
|
||||
newPrices,
|
||||
}) {
|
||||
const fmtPrice = (p) => `${p.net ?? p.price}@${p.taxRate ?? p.tax ?? 0}%`;
|
||||
const oldStr = (oldPrices || []).map(fmtPrice).join(', ');
|
||||
const newStr = (newPrices || []).map(fmtPrice).join(', ');
|
||||
if (oldStr === newStr) return;
|
||||
|
||||
const text = `Preise geändert von [${oldStr}] auf [${newStr}]`;
|
||||
const meta = {
|
||||
type: 'price_change',
|
||||
oldPrices,
|
||||
newPrices,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records category assignment changes in article history.
|
||||
*/
|
||||
export async function recordCategoryChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldCategoryIds,
|
||||
newCategoryIds,
|
||||
}) {
|
||||
const oldStr = [...(oldCategoryIds || [])].sort((a, b) => a - b).join(', ');
|
||||
const newStr = [...(newCategoryIds || [])].sort((a, b) => a - b).join(', ');
|
||||
if (oldStr === newStr) return;
|
||||
|
||||
const text = `Kategorien geändert von [${oldStr}] auf [${newStr}]`;
|
||||
const meta = {
|
||||
type: 'category_change',
|
||||
oldCategoryIds,
|
||||
newCategoryIds,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records brand changes in article history.
|
||||
*/
|
||||
export async function recordBrandChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldBrandName,
|
||||
newBrandName,
|
||||
}) {
|
||||
if (oldBrandName === newBrandName) return;
|
||||
|
||||
const text = `Marke geändert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`;
|
||||
const meta = {
|
||||
type: 'brand_change',
|
||||
oldBrandName: oldBrandName ?? null,
|
||||
newBrandName: newBrandName ?? null,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records EAN changes in article history.
|
||||
*/
|
||||
export async function recordEanChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldEans,
|
||||
newEans,
|
||||
}) {
|
||||
const oldStr = (oldEans || []).join(', ');
|
||||
const newStr = [...(newEans || [])].sort().join(', ');
|
||||
if (oldStr === newStr) return;
|
||||
|
||||
const text = `EANs geändert von "${oldStr}" auf "${newStr}"`;
|
||||
const meta = {
|
||||
type: 'ean_change',
|
||||
oldEans,
|
||||
newEans,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records weight changes in article history.
|
||||
*/
|
||||
export async function recordWeightChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldWeight,
|
||||
newWeight,
|
||||
}) {
|
||||
if (Number(oldWeight) === Number(newWeight)) return;
|
||||
|
||||
const text = `Gewicht geändert von "${oldWeight}" auf "${newWeight}"`;
|
||||
const meta = {
|
||||
type: 'weight_change',
|
||||
oldWeight: Number(oldWeight),
|
||||
newWeight: Number(newWeight),
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records track_stock changes in article history.
|
||||
*/
|
||||
export async function recordTrackStockChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
oldTrack,
|
||||
newTrack,
|
||||
}) {
|
||||
if (Boolean(oldTrack) === Boolean(newTrack)) return;
|
||||
|
||||
const text = `Bestandsführung geändert von "${oldTrack ? 'Ja' : 'Nein'}" auf "${newTrack ? 'Ja' : 'Nein'}"`;
|
||||
const meta = {
|
||||
type: 'track_stock_change',
|
||||
oldTrackStock: Boolean(oldTrack),
|
||||
newTrackStock: Boolean(newTrack),
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records general article field updates (name, unit, description, track_stock).
|
||||
*/
|
||||
export async function recordArticleUpdateHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
historyLines,
|
||||
changes,
|
||||
}) {
|
||||
if (!historyLines || !historyLines.length) return;
|
||||
|
||||
const text = historyLines.join('\n');
|
||||
const meta = {
|
||||
type: 'article_update',
|
||||
changes: changes ?? historyLines,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records initial article creation.
|
||||
*/
|
||||
export async function recordArticleCreatedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
name,
|
||||
}) {
|
||||
const text = `Artikel "${name}" angelegt`;
|
||||
const meta = {
|
||||
type: 'article_created',
|
||||
name,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records article import.
|
||||
*/
|
||||
export async function recordArticleImportedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
name,
|
||||
}) {
|
||||
const text = `Artikel "${name}" importiert`;
|
||||
const meta = {
|
||||
type: 'article_imported',
|
||||
name,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
date,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records conversion of an article to parent article.
|
||||
*/
|
||||
export async function recordParentConvertedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
attributeName,
|
||||
existingPrices,
|
||||
}) {
|
||||
const historyLines = [
|
||||
attributeName
|
||||
? `Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`
|
||||
: 'Artikel zu Vaterartikel konvertiert',
|
||||
];
|
||||
if (existingPrices && existingPrices.length > 0) {
|
||||
const priceParts = existingPrices.map((p) => {
|
||||
let str = `${p.net} €`;
|
||||
if (p.taxRate != null) str += ` (${p.taxRate}% MwSt.)`;
|
||||
if (p.minmultiplier > 1) str += ` ab ${p.minmultiplier} Stk.`;
|
||||
if (p.customerGroupName) str += ` [${p.customerGroupName}]`;
|
||||
return str;
|
||||
});
|
||||
const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht';
|
||||
historyLines.push(`${label}: ${priceParts.join(', ')}`);
|
||||
}
|
||||
const text = historyLines.join('\n');
|
||||
const meta = {
|
||||
type: 'parent_converted',
|
||||
attributeName: attributeName ?? null,
|
||||
deletedPricesCount: existingPrices ? existingPrices.length : 0,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records deactivation of parent article mode.
|
||||
*/
|
||||
export async function recordParentDeactivatedHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
}) {
|
||||
const text = 'Vaterartikel-Modus deaktiviert';
|
||||
const meta = {
|
||||
type: 'parent_deactivated',
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records adding a variant onto a parent article.
|
||||
*/
|
||||
export async function recordVariantAddedOnParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
childName,
|
||||
childSku,
|
||||
}) {
|
||||
const text = `Variante "${childName}" (${childSku}) hinzugefügt`;
|
||||
const meta = {
|
||||
type: 'variant_added_to_parent',
|
||||
childName,
|
||||
childSku,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records assigning a child article to a parent article.
|
||||
*/
|
||||
export async function recordChildAssignedToParentHistory(runner, {
|
||||
childId,
|
||||
companyuserId,
|
||||
childName,
|
||||
childSku,
|
||||
parentSku,
|
||||
created = false,
|
||||
}) {
|
||||
const text = created
|
||||
? `Artikel "${childName}" (${childSku}) als Variante von "${parentSku}" angelegt`
|
||||
: (childName
|
||||
? `Artikel "${childName}" (${childSku}) als Variante von "${parentSku}" zugewiesen`
|
||||
: `Artikel "${childSku}" als Variante von "${parentSku}" zugewiesen`);
|
||||
const meta = {
|
||||
type: created ? 'child_created_under_parent' : 'child_assigned_to_parent',
|
||||
childName: childName ?? null,
|
||||
childSku,
|
||||
parentSku,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId: childId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records unlinking a child article from a parent article on both child and parent.
|
||||
*/
|
||||
export async function recordChildUnlinkedFromParentHistory(runner, {
|
||||
childId,
|
||||
parentId,
|
||||
companyuserId,
|
||||
childSku,
|
||||
childName,
|
||||
parentSku,
|
||||
}) {
|
||||
await recordArticleHistory(runner, {
|
||||
articleId: childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
|
||||
meta: {
|
||||
type: 'child_unlinked',
|
||||
childSku,
|
||||
parentSku,
|
||||
},
|
||||
});
|
||||
|
||||
await recordArticleHistory(runner, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName || childSku}" (${childSku}) getrennt`,
|
||||
meta: {
|
||||
type: 'variant_separated',
|
||||
childSku,
|
||||
childName: childName ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records deleting a variant child article from a parent.
|
||||
*/
|
||||
export async function recordChildDeletedFromParentHistory(runner, {
|
||||
parentId,
|
||||
companyuserId,
|
||||
childName,
|
||||
childSku,
|
||||
}) {
|
||||
const text = `Variante "${childName || childSku}" (${childSku}) gelöscht`;
|
||||
const meta = {
|
||||
type: 'variant_deleted',
|
||||
childName: childName ?? null,
|
||||
childSku,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId: parentId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records adding a supplier to an article.
|
||||
*/
|
||||
export async function recordSupplierAddHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
supplierName,
|
||||
supplierSku,
|
||||
netPrice,
|
||||
}) {
|
||||
const details = [];
|
||||
if (supplierSku) details.push(`Artikelnr: "${supplierSku}"`);
|
||||
if (netPrice !== null && netPrice !== undefined) details.push(`EK: ${netPrice}`);
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
const text = `Lieferant "${supplierName}" hinzugefügt${suffix}`;
|
||||
const meta = {
|
||||
type: 'supplier_add',
|
||||
supplierName,
|
||||
supplierSku: supplierSku ?? null,
|
||||
netPrice: netPrice ?? null,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records updating supplier details on an article.
|
||||
*/
|
||||
export async function recordSupplierUpdateHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
historyLines,
|
||||
supplierName,
|
||||
}) {
|
||||
if (!historyLines || !historyLines.length) return;
|
||||
const text = historyLines.join('\n');
|
||||
const meta = {
|
||||
type: 'supplier_update',
|
||||
supplierName,
|
||||
changes: historyLines,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records deleting a supplier from an article.
|
||||
*/
|
||||
export async function recordSupplierDeleteHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
supplierName,
|
||||
supplierSku,
|
||||
}) {
|
||||
const suffix = supplierSku ? ` (Artikelnr: "${supplierSku}")` : '';
|
||||
const text = `Lieferant "${supplierName}" entfernt${suffix}`;
|
||||
const meta = {
|
||||
type: 'supplier_delete',
|
||||
supplierName,
|
||||
supplierSku: supplierSku ?? null,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records property changes on an article.
|
||||
*/
|
||||
export async function recordPropertyChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldProps,
|
||||
newProps,
|
||||
}) {
|
||||
const fmtProp = (p) => `${p.name}=${p.value}`;
|
||||
const oldStr = (oldProps || []).map(fmtProp).join(', ');
|
||||
const newStr = (newProps || []).map(fmtProp).join(', ');
|
||||
if (oldStr === newStr) return;
|
||||
|
||||
const text = `Eigenschaften geändert von [${oldStr}] auf [${newStr}]`;
|
||||
const meta = {
|
||||
type: 'property_change',
|
||||
oldProps,
|
||||
newProps,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records property deletion from an article.
|
||||
*/
|
||||
export async function recordPropertyDeleteHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
name,
|
||||
value,
|
||||
}) {
|
||||
const text = `Eigenschaft "${name}" (${value}) gelöscht`;
|
||||
const meta = {
|
||||
type: 'property_delete',
|
||||
name,
|
||||
value,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records attribute value changes on an article.
|
||||
*/
|
||||
export async function recordAttributeChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldAttrs,
|
||||
newAttrs,
|
||||
}) {
|
||||
const fmtAttr = (a) => `${a.attributeName}: ${a.value}`;
|
||||
const oldStr = (oldAttrs || []).map(fmtAttr).join(', ');
|
||||
const newStr = (newAttrs || []).map(fmtAttr).join(', ');
|
||||
if (oldStr === newStr) return;
|
||||
|
||||
const text = `Attributwerte geändert von [${oldStr}] auf [${newStr}]`;
|
||||
const meta = {
|
||||
type: 'attribute_change',
|
||||
oldAttrs,
|
||||
newAttrs,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records Stückliste (component items) assignment on an article.
|
||||
*/
|
||||
export async function recordArticleListChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
items,
|
||||
}) {
|
||||
const newNames = (items || []).map((r) => `${r.count}x ${r.name || r.sku}`);
|
||||
const text = `Stückliste gesetzt: [${newNames.join(', ')}]`;
|
||||
const meta = {
|
||||
type: 'articlelist_change',
|
||||
items,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records Optionsliste assignment on an article.
|
||||
*/
|
||||
export async function recordOptionsListChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
options,
|
||||
}) {
|
||||
const newNames = (options || []).map((r) => r.name || r.sku);
|
||||
const text = `Optionsliste gesetzt: [${newNames.join(', ')}]`;
|
||||
const meta = {
|
||||
type: 'optionslist_change',
|
||||
options,
|
||||
};
|
||||
return recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/articlelist.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articlelist.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import getItemsByArticleSku from './getItemsByArticleSku.js';
|
||||
import {
|
||||
recordArticleListChangeHistory,
|
||||
recordArticleHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
async function hasArticlelistCycle(runner, rootArticleId, candidateComponentId) {
|
||||
@@ -44,7 +46,6 @@ export default function setItemsByArticleSku(provider) {
|
||||
const getItems = getItemsByArticleSku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Sets or clears the Stückliste (bundle items) for an article.
|
||||
@@ -100,10 +101,11 @@ export default function setItemsByArticleSku(provider) {
|
||||
});
|
||||
|
||||
if (oldItemNames.length) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Stückliste geleert (war: [${oldItemNames.join(', ')}])`,
|
||||
type: 'articlelist_cleared',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -221,11 +223,10 @@ export default function setItemsByArticleSku(provider) {
|
||||
const result = await getItems(googleAuthId, sku, runner);
|
||||
|
||||
// Write history for Stückliste change
|
||||
const newNames = result.map((r) => `${r.count}x ${r.name || r.sku}`);
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleListChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Stückliste gesetzt: [${newNames.join(', ')}]`,
|
||||
items: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
import {
|
||||
DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE,
|
||||
} from '../../providers/sqlite3/queries/articleproperty.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import { recordPropertyDeleteHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function deleteArticleProperty(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Deletes a property from an article by SKU and propertyId.
|
||||
@@ -41,10 +39,11 @@ export default function deleteArticleProperty(provider) {
|
||||
await runner.query(deleteSql, { articleId, propertyId });
|
||||
|
||||
if (prop) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordPropertyDeleteHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Eigenschaft "${prop.name}" (${prop.value}) gelöscht`,
|
||||
name: prop.name,
|
||||
value: prop.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
import {
|
||||
GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP,
|
||||
} from '../../providers/sqlite3/queries/propertyname.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import { recordPropertyChangeHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function setByArticleSku(provider) {
|
||||
@@ -23,7 +22,6 @@ export default function setByArticleSku(provider) {
|
||||
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
const getPropSql = isSqlite ? SQLITE_GET_PROP : MSSQL_GET_PROP;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Replaces all properties for an article by its SKU.
|
||||
@@ -119,16 +117,12 @@ export default function setByArticleSku(provider) {
|
||||
}
|
||||
|
||||
// Write history if properties changed
|
||||
const fmtProp = (p) => `${p.name}=${p.value}`;
|
||||
const oldStr = oldProps.map(fmtProp).join(', ');
|
||||
const newStr = validated.map(fmtProp).join(', ');
|
||||
if (oldStr !== newStr) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Eigenschaften geändert von [${oldStr}] auf [${newStr}]`,
|
||||
});
|
||||
}
|
||||
await recordPropertyChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
oldProps,
|
||||
newProps: validated,
|
||||
});
|
||||
|
||||
return inserted;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import {
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
resolveCompanyDefaultTax,
|
||||
shapeArticleSupplier,
|
||||
} from './helpers.js';
|
||||
import { recordSupplierAddHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function create(provider) {
|
||||
@@ -18,7 +17,6 @@ export default function create(provider) {
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Assigns a supplier to an article, optionally creating a purchase price.
|
||||
@@ -150,21 +148,15 @@ export default function create(provider) {
|
||||
const supplierName =
|
||||
supplierRow?.supplierName ?? (entityId ? `Lieferant #${entityId}` : 'Lieferant');
|
||||
|
||||
const details = [];
|
||||
const actualSku = supplierRow?.sku ?? supplierSku;
|
||||
if (actualSku) {
|
||||
details.push(`Artikelnr: "${actualSku}"`);
|
||||
}
|
||||
const actualPrice = supplierRow?.priceNet ?? netPrice;
|
||||
if (actualPrice !== null && actualPrice !== undefined) {
|
||||
details.push(`EK: ${actualPrice}`);
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordSupplierAddHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Lieferant "${supplierName}" hinzugefügt${suffix}`,
|
||||
supplierName,
|
||||
supplierSku: actualSku,
|
||||
netPrice: actualPrice,
|
||||
});
|
||||
|
||||
return shapeArticleSupplier(supplierRow);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import { recordSupplierDeleteHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function del(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Deletes a supplier assignment and cleans up its associated purchase price.
|
||||
@@ -55,16 +53,11 @@ export default function del(provider) {
|
||||
const supplierName =
|
||||
existing.supplierName ?? (existing.entityId ? `Lieferant #${existing.entityId}` : 'Lieferant');
|
||||
|
||||
const details = [];
|
||||
if (existing.sku) {
|
||||
details.push(`Artikelnr: "${existing.sku}"`);
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordSupplierDeleteHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Lieferant "${supplierName}" entfernt${suffix}`,
|
||||
supplierName,
|
||||
supplierSku: existing.sku,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import {
|
||||
@@ -9,6 +7,7 @@ import {
|
||||
resolveCompanyDefaultTax,
|
||||
shapeArticleSupplier,
|
||||
} from './helpers.js';
|
||||
import { recordSupplierUpdateHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function update(provider) {
|
||||
@@ -17,7 +16,6 @@ export default function update(provider) {
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Updates an existing supplier assignment and its associated purchase price.
|
||||
@@ -213,13 +211,12 @@ export default function update(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
if (historyLines.length) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: historyLines.join('\n'),
|
||||
});
|
||||
}
|
||||
await recordSupplierUpdateHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
historyLines,
|
||||
supplierName,
|
||||
});
|
||||
|
||||
const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
|
||||
id,
|
||||
|
||||
@@ -1,52 +1,60 @@
|
||||
import { GET_TREE_FOR_COMPANY as MSSQL_GET_TREE } from '../../providers/mssql/queries/category.js';
|
||||
import { GET_TREE_FOR_COMPANY as SQLITE_GET_TREE } from '../../providers/sqlite3/queries/category.js';
|
||||
import {
|
||||
GET_TREE_FOR_COMPANY as MSSQL_GET_TREE,
|
||||
COUNT_ARTICLES_IN_TREE as MSSQL_COUNT_ARTICLES,
|
||||
} from '../../providers/mssql/queries/category.js';
|
||||
import {
|
||||
GET_TREE_FOR_COMPANY as SQLITE_GET_TREE,
|
||||
COUNT_ARTICLES_IN_TREE as SQLITE_COUNT_ARTICLES,
|
||||
} from '../../providers/sqlite3/queries/category.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import queryCategoryArticles from './queryCategoryArticles.js';
|
||||
import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function getTreeForCompany(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const articles = queryCategoryArticles(provider);
|
||||
const posterminalIds = getCategoryPosterminalIds(provider);
|
||||
const sql =
|
||||
provider?.dialect === 'sqlite3' ? SQLITE_GET_TREE : MSSQL_GET_TREE;
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const sql = isSqlite ? SQLITE_GET_TREE : MSSQL_GET_TREE;
|
||||
const countSql = isSqlite ? SQLITE_COUNT_ARTICLES : MSSQL_COUNT_ARTICLES;
|
||||
|
||||
/**
|
||||
* Returns the full category tree for the user's selected company together
|
||||
* with its articles and the posterminal ids assigned to each category.
|
||||
* with the article count and the posterminal ids assigned to each category.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>} The category tree, articles and language.
|
||||
* @returns {Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>} The category tree and article count.
|
||||
*
|
||||
* @example
|
||||
* const tree = await store.category.getTreeForCompany(42);
|
||||
* // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
||||
* // => { languageId, tree: [{ id, articleCount, posterminalIds, ... }], articleCount: 25 }
|
||||
*/
|
||||
return async (googleAuthId, tx) => {
|
||||
const { companyId, rootCategoryId, languageId, posCustomerGroupId } =
|
||||
const { companyId, rootCategoryId, languageId } =
|
||||
await access(googleAuthId, tx);
|
||||
|
||||
const runner = makeRunner(provider, tx);
|
||||
const [treeResult, articleRows, ptMap] = await Promise.all([
|
||||
const [treeResult, countResult, ptMap] = await Promise.all([
|
||||
runner.query(sql, {
|
||||
rootId: rootCategoryId,
|
||||
languageId,
|
||||
}),
|
||||
articles({ categoryId: rootCategoryId, languageId, posCustomerGroupId }, tx),
|
||||
runner.query(countSql, {
|
||||
rootId: rootCategoryId,
|
||||
}),
|
||||
posterminalIds(companyId, tx),
|
||||
]);
|
||||
|
||||
const tree = treeResult.rows.map((row) => ({
|
||||
...row,
|
||||
articleCount: Number(row.articleCount ?? 0),
|
||||
posterminalIds: ptMap[row.id] ?? [],
|
||||
}));
|
||||
|
||||
return {
|
||||
languageId,
|
||||
tree,
|
||||
articles: articleRows,
|
||||
articleCount: Number(countResult.rows[0]?.total ?? 0),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ export default function list(provider) {
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<Array<object>>} The accessible company rows.
|
||||
* @returns {Promise<Array<{id: number, name: string, rootCategoryId: number, languageId: number, iso639: string|null, taxrate: number, reducedtaxrate: number, taxfreerate: number}>>} The accessible company rows.
|
||||
*
|
||||
* @example
|
||||
* const companies = await store.company.list(42);
|
||||
* // => [{ id, name, ... }]
|
||||
* // => [{ id, name, rootCategoryId, languageId, iso639, taxrate, reducedtaxrate, taxfreerate }]
|
||||
*/
|
||||
return async (googleAuthId, tx) => {
|
||||
const result = await makeRunner(provider, tx).query(sql, { googleAuthId });
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { recordDeliveryHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
|
||||
@@ -205,42 +206,20 @@ export default function add(provider) {
|
||||
!upperType.includes('LOSS') &&
|
||||
!upperType.includes('WRITE_OFF'));
|
||||
|
||||
let historyText = transactionText;
|
||||
if (isDelivery) {
|
||||
const details = [];
|
||||
if (manifestArticle.supplierName) {
|
||||
details.push(`Lieferant: "${manifestArticle.supplierName}"`);
|
||||
}
|
||||
if (warehouseName) {
|
||||
details.push(`Lager: "${warehouseName}"`);
|
||||
}
|
||||
if (manifestArticle.net != null && Number.isFinite(Number(manifestArticle.net))) {
|
||||
const net = Number(manifestArticle.net);
|
||||
const rebate = Number(manifestArticle.rebate ?? 0);
|
||||
const taxrate = Number(manifestArticle.taxrate ?? 0);
|
||||
const netAfterRebate = net * (1 - rebate / 100);
|
||||
const brutto = netAfterRebate * (1 + taxrate / 100);
|
||||
const ekBrutto = (Math.round(brutto * 100) / 100).toFixed(2);
|
||||
details.push(`EK brutto: ${ekBrutto}`);
|
||||
}
|
||||
if (amount != null && Number.isFinite(Number(amount))) {
|
||||
details.push(`Menge: ${amount}`);
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
const base =
|
||||
transactionText?.trim() ||
|
||||
(manifestArticle.manifestNumber ? `Lieferung ${manifestArticle.manifestNumber}` : 'Lieferung');
|
||||
historyText = `${base}${suffix}`;
|
||||
} else if (warehouseName && !transactionText.includes(warehouseName)) {
|
||||
historyText = `${transactionText} (Lager: "${warehouseName}")`;
|
||||
}
|
||||
|
||||
if (!isShipping) {
|
||||
for (const match of articleMatches.rows) {
|
||||
await runner.query(queries.INSERT_ARTICLE_HISTORY, {
|
||||
await recordDeliveryHistory(runner, {
|
||||
articleId: match.articleId,
|
||||
companyuserId,
|
||||
text: historyText,
|
||||
manifestNumber: manifestArticle.manifestNumber,
|
||||
supplierName: manifestArticle.supplierName,
|
||||
warehouseName,
|
||||
net: manifestArticle.net,
|
||||
rebate: manifestArticle.rebate,
|
||||
taxrate: manifestArticle.taxrate,
|
||||
amount,
|
||||
transactionText,
|
||||
isDelivery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
16
src/store/mcp/consumeAuthCode.js
Normal file
16
src/store/mcp/consumeAuthCode.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CONSUME_AUTH_CODE as MSSQL_CONSUME } from '../../providers/mssql/queries/mcp.js';
|
||||
import { CONSUME_AUTH_CODE as SQLITE_CONSUME } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function consumeAuthCode(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_CONSUME : MSSQL_CONSUME;
|
||||
/**
|
||||
* Marks an authorization code as used.
|
||||
*
|
||||
* @param {string} code - The authorization code to mark used.
|
||||
* @returns {Promise<boolean>} True once marked.
|
||||
*/
|
||||
return async (code) => {
|
||||
await provider.query(sql, { code });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
43
src/store/mcp/createAuthCode.js
Normal file
43
src/store/mcp/createAuthCode.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { INSERT_AUTH_CODE as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js';
|
||||
import { INSERT_AUTH_CODE as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function createAuthCode(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
/**
|
||||
* Stores an OAuth 2.1 authorization code with PKCE parameters.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {string} input.code - The generated authorization code.
|
||||
* @param {string} input.clientId - The client ID.
|
||||
* @param {number} input.googleAuthId - The ID of the authenticated user.
|
||||
* @param {string} input.redirectUri - The redirect URI used in the authorize request.
|
||||
* @param {string} input.codeChallenge - The PKCE code challenge.
|
||||
* @param {string} input.codeChallengeMethod - The PKCE method (e.g. S256).
|
||||
* @param {string|null} [input.scope] - Granted scope.
|
||||
* @param {string|Date} input.expiresAt - Expiration ISO string or Date.
|
||||
* @returns {Promise<string>} The authorization code.
|
||||
*/
|
||||
return async ({
|
||||
code,
|
||||
clientId,
|
||||
googleAuthId,
|
||||
redirectUri,
|
||||
codeChallenge,
|
||||
codeChallengeMethod = 'S256',
|
||||
scope = 'mcp:tools',
|
||||
expiresAt,
|
||||
}) => {
|
||||
const exp = expiresAt instanceof Date ? expiresAt.toISOString() : expiresAt;
|
||||
await provider.query(sql, {
|
||||
code,
|
||||
clientId,
|
||||
googleAuthId,
|
||||
redirectUri,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
scope,
|
||||
expiresAt: exp,
|
||||
});
|
||||
return code;
|
||||
};
|
||||
}
|
||||
34
src/store/mcp/createRefreshToken.js
Normal file
34
src/store/mcp/createRefreshToken.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { INSERT_REFRESH_TOKEN as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js';
|
||||
import { INSERT_REFRESH_TOKEN as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function createRefreshToken(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
/**
|
||||
* Stores an OAuth 2.1 refresh token.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {string} input.refreshToken - The refresh token string.
|
||||
* @param {string} input.clientId - The client ID.
|
||||
* @param {number} input.googleAuthId - The user ID.
|
||||
* @param {string|null} [input.scope] - Scopes granted.
|
||||
* @param {string|Date} input.expiresAt - Expiration Date or ISO string.
|
||||
* @returns {Promise<object>} Created token record or id.
|
||||
*/
|
||||
return async ({
|
||||
refreshToken,
|
||||
clientId,
|
||||
googleAuthId,
|
||||
scope = 'mcp:tools',
|
||||
expiresAt,
|
||||
}) => {
|
||||
const exp = expiresAt instanceof Date ? expiresAt.toISOString() : expiresAt;
|
||||
const result = await provider.query(sql, {
|
||||
refreshToken,
|
||||
clientId,
|
||||
googleAuthId,
|
||||
scope,
|
||||
expiresAt: exp,
|
||||
});
|
||||
return result.rows[0]?.id ?? result.insertId;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/deleteClient.js
Normal file
16
src/store/mcp/deleteClient.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DELETE_CLIENT as MSSQL_DELETE } from '../../providers/mssql/queries/mcp.js';
|
||||
import { DELETE_CLIENT as SQLITE_DELETE } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function deleteClient(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
/**
|
||||
* Deletes a registered MCP client by client ID.
|
||||
*
|
||||
* @param {string} clientId - The client identifier to delete.
|
||||
* @returns {Promise<boolean>} True if deleted.
|
||||
*/
|
||||
return async (clientId) => {
|
||||
await provider.query(sql, { clientId });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/getAuthCode.js
Normal file
16
src/store/mcp/getAuthCode.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GET_AUTH_CODE as MSSQL_GET } from '../../providers/mssql/queries/mcp.js';
|
||||
import { GET_AUTH_CODE as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function getAuthCode(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET;
|
||||
/**
|
||||
* Retrieves an authorization code record.
|
||||
*
|
||||
* @param {string} code - The authorization code.
|
||||
* @returns {Promise<object|null>} The auth code record or null.
|
||||
*/
|
||||
return async (code) => {
|
||||
const result = await provider.query(sql, { code });
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/getClient.js
Normal file
16
src/store/mcp/getClient.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GET_CLIENT as MSSQL_GET } from '../../providers/mssql/queries/mcp.js';
|
||||
import { GET_CLIENT as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function getClient(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET;
|
||||
/**
|
||||
* Retrieves an MCP client by client ID.
|
||||
*
|
||||
* @param {string} clientId - The client identifier.
|
||||
* @returns {Promise<object|null>} The client record or null if not found.
|
||||
*/
|
||||
return async (clientId) => {
|
||||
const result = await provider.query(sql, { clientId });
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/getRefreshToken.js
Normal file
16
src/store/mcp/getRefreshToken.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GET_REFRESH_TOKEN as MSSQL_GET } from '../../providers/mssql/queries/mcp.js';
|
||||
import { GET_REFRESH_TOKEN as SQLITE_GET } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function getRefreshToken(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET;
|
||||
/**
|
||||
* Retrieves an OAuth 2.1 refresh token.
|
||||
*
|
||||
* @param {string} refreshToken - The refresh token.
|
||||
* @returns {Promise<object|null>} The refresh token record or null.
|
||||
*/
|
||||
return async (refreshToken) => {
|
||||
const result = await provider.query(sql, { refreshToken });
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/listAuthorizations.js
Normal file
16
src/store/mcp/listAuthorizations.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { LIST_AUTHORIZATIONS_BY_USER as MSSQL_LIST } from '../../providers/mssql/queries/mcp.js';
|
||||
import { LIST_AUTHORIZATIONS_BY_USER as SQLITE_LIST } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function listAuthorizations(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST;
|
||||
/**
|
||||
* Lists active authorizations for a Google Auth user.
|
||||
*
|
||||
* @param {number} googleAuthId - The user ID.
|
||||
* @returns {Promise<Array<object>>} List of authorized clients and token counts.
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const result = await provider.query(sql, { googleAuthId });
|
||||
return result.rows ?? [];
|
||||
};
|
||||
}
|
||||
15
src/store/mcp/listClients.js
Normal file
15
src/store/mcp/listClients.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { LIST_CLIENTS as MSSQL_LIST } from '../../providers/mssql/queries/mcp.js';
|
||||
import { LIST_CLIENTS as SQLITE_LIST } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function listClients(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST;
|
||||
/**
|
||||
* Lists all registered MCP clients.
|
||||
*
|
||||
* @returns {Promise<Array<object>>} List of client records.
|
||||
*/
|
||||
return async () => {
|
||||
const result = await provider.query(sql);
|
||||
return result.rows ?? [];
|
||||
};
|
||||
}
|
||||
55
src/store/mcp/registerClient.js
Normal file
55
src/store/mcp/registerClient.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { INSERT_CLIENT as MSSQL_INSERT } from '../../providers/mssql/queries/mcp.js';
|
||||
import { INSERT_CLIENT as SQLITE_INSERT } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function registerClient(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_INSERT : MSSQL_INSERT;
|
||||
/**
|
||||
* Registers a new OAuth MCP client.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {string} input.clientId - Unique client identifier.
|
||||
* @param {string|null} [input.clientSecret] - Optional client secret for confidential clients.
|
||||
* @param {string} input.clientName - Human-readable client name.
|
||||
* @param {string|Array<string>} input.redirectUris - JSON string or array of redirect URIs.
|
||||
* @param {string|Array<string>} [input.grantTypes] - Supported grant types.
|
||||
* @param {string|Array<string>} [input.responseTypes] - Supported response types.
|
||||
* @param {string|null} [input.scope] - Allowed scope.
|
||||
* @returns {Promise<object>} The created client record.
|
||||
*/
|
||||
return async ({
|
||||
clientId,
|
||||
clientSecret = null,
|
||||
clientName,
|
||||
redirectUris,
|
||||
grantTypes = 'authorization_code,refresh_token',
|
||||
responseTypes = 'code',
|
||||
scope = 'mcp:tools',
|
||||
}) => {
|
||||
const urisStr = Array.isArray(redirectUris) ? JSON.stringify(redirectUris) : redirectUris;
|
||||
const grantsStr = Array.isArray(grantTypes) ? grantTypes.join(',') : grantTypes;
|
||||
const resTypesStr = Array.isArray(responseTypes) ? responseTypes.join(',') : responseTypes;
|
||||
|
||||
const result = await provider.query(sql, {
|
||||
clientId,
|
||||
clientSecret,
|
||||
clientName,
|
||||
redirectUris: urisStr,
|
||||
grantTypes: grantsStr,
|
||||
responseTypes: resTypesStr,
|
||||
scope,
|
||||
});
|
||||
|
||||
const row = result.rows[0];
|
||||
return {
|
||||
id: row?.id ?? result.insertId,
|
||||
clientId: row?.clientId ?? row?.client_id ?? clientId,
|
||||
clientSecret: row?.clientSecret ?? row?.client_secret ?? clientSecret,
|
||||
clientName: row?.clientName ?? row?.client_name ?? clientName,
|
||||
redirectUris: row?.redirectUris ?? row?.redirect_uris ?? urisStr,
|
||||
grantTypes: row?.grantTypes ?? row?.grant_types ?? grantsStr,
|
||||
responseTypes: row?.responseTypes ?? row?.response_types ?? resTypesStr,
|
||||
scope: row?.scope ?? scope,
|
||||
createdAt: row?.createdAt ?? row?.created_at,
|
||||
};
|
||||
};
|
||||
}
|
||||
17
src/store/mcp/revokeAuthorizations.js
Normal file
17
src/store/mcp/revokeAuthorizations.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { REVOKE_USER_AUTHORIZATIONS as MSSQL_REVOKE } from '../../providers/mssql/queries/mcp.js';
|
||||
import { REVOKE_USER_AUTHORIZATIONS as SQLITE_REVOKE } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function revokeAuthorizations(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_REVOKE : MSSQL_REVOKE;
|
||||
/**
|
||||
* Revokes all active authorizations (tokens) for a user and MCP client.
|
||||
*
|
||||
* @param {number} googleAuthId - The user ID.
|
||||
* @param {string} clientId - The MCP client ID.
|
||||
* @returns {Promise<boolean>} True once revoked.
|
||||
*/
|
||||
return async (googleAuthId, clientId) => {
|
||||
await provider.query(sql, { googleAuthId, clientId });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
16
src/store/mcp/revokeRefreshToken.js
Normal file
16
src/store/mcp/revokeRefreshToken.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { REVOKE_REFRESH_TOKEN as MSSQL_REVOKE } from '../../providers/mssql/queries/mcp.js';
|
||||
import { REVOKE_REFRESH_TOKEN as SQLITE_REVOKE } from '../../providers/sqlite3/queries/mcp.js';
|
||||
|
||||
export default function revokeRefreshToken(provider) {
|
||||
const sql = provider.dialect === 'sqlite3' ? SQLITE_REVOKE : MSSQL_REVOKE;
|
||||
/**
|
||||
* Revokes an OAuth 2.1 refresh token.
|
||||
*
|
||||
* @param {string} refreshToken - The refresh token to revoke.
|
||||
* @returns {Promise<boolean>} True once revoked.
|
||||
*/
|
||||
return async (refreshToken) => {
|
||||
await provider.query(sql, { refreshToken });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -15,9 +15,10 @@ export default function create(provider) {
|
||||
* @param {object} input - Conversation input.
|
||||
* @param {string} input.provider - LLM provider name.
|
||||
* @param {string} input.model - Model identifier.
|
||||
* @param {number|null} [input.started_by] - Id of the conversation that starts this one as a subagent (unenforced reference; omit/null for a top-level conversation).
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>}
|
||||
* @throws {Error} 'Provider required' or 'Model required'.
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>}
|
||||
* @throws {Error} 'Provider required', 'Model required' or 'Invalid started_by'.
|
||||
*
|
||||
* @example
|
||||
* const c = await store.modelconversation.create(42, { provider: 'openrouter', model: 'openai/gpt-5' });
|
||||
@@ -28,6 +29,10 @@ export default function create(provider) {
|
||||
if (!providerName) throw new Error('Provider required');
|
||||
const model = typeof input.model === 'string' ? input.model.trim() : '';
|
||||
if (!model) throw new Error('Model required');
|
||||
const startedBy = input.started_by ?? null;
|
||||
if (startedBy !== null && (!Number.isInteger(startedBy) || startedBy <= 0)) {
|
||||
throw new Error('Invalid started_by');
|
||||
}
|
||||
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyuserId } = await access(googleAuthId, runner);
|
||||
@@ -36,6 +41,7 @@ export default function create(provider) {
|
||||
companyuserId,
|
||||
provider: providerName,
|
||||
model,
|
||||
startedBy,
|
||||
});
|
||||
return shape(result.rows[0]);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ export default function del(provider) {
|
||||
provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
|
||||
/**
|
||||
* Deletes a model conversation by ID for the user in the selected company.
|
||||
* Deletes a model conversation by ID for the user in the selected company, together with
|
||||
* every subagent conversation below it (`started_by` chain, recursively) and their messages.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} conversationId - The conversation id.
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function get(provider) {
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} conversationId - The conversation id.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>}
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>} `first_user_message` is the stored `message` payload of the first `user` message (null if none yet).
|
||||
* @throws {Error} 'Conversation not found'.
|
||||
*/
|
||||
return async (googleAuthId, conversationId, tx) => {
|
||||
@@ -28,6 +28,6 @@ export default function get(provider) {
|
||||
|
||||
const result = await runner.query(sql, { id, companyuserId });
|
||||
if (!result.rows[0]) throw new Error('Conversation not found');
|
||||
return shape(result.rows[0]);
|
||||
return { ...shape(result.rows[0]), first_user_message: result.rows[0].first_user_message ?? null };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function list(provider) {
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>}
|
||||
* @returns {Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>>} `started_by` is the originating conversation id for subagent conversations (null otherwise), so clients can build a tree. `first_user_message` is the stored `message` payload of the conversation's first `user` message (null if none yet).
|
||||
*
|
||||
* @example
|
||||
* const conversations = await store.modelconversation.list(42);
|
||||
@@ -22,6 +22,9 @@ export default function list(provider) {
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyuserId } = await access(googleAuthId, runner);
|
||||
const result = await runner.query(sql, { companyuserId });
|
||||
return result.rows.map(shape);
|
||||
return result.rows.map((row) => ({
|
||||
...shape(row),
|
||||
first_user_message: row.first_user_message ?? null,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function setState(provider) {
|
||||
* @param {number} conversationId - The conversation id.
|
||||
* @param {string} state - The new state ('idle' | 'busy').
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>}
|
||||
* @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>}
|
||||
* @throws {Error} 'Invalid state' or 'Conversation not found'.
|
||||
*/
|
||||
return async (googleAuthId, conversationId, state, tx) => {
|
||||
|
||||
@@ -7,5 +7,6 @@ export default function shape(row) {
|
||||
created_at: row.created_at ?? row.createdAt,
|
||||
updated_at: row.updated_at ?? row.updatedAt,
|
||||
state: row.state,
|
||||
started_by: row.started_by ?? row.startedBy ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
const DEFAULT_TYPES = ['sku', 'entity', 'delivery', 'adjustment'];
|
||||
const DEFAULT_TYPES = [
|
||||
{ type: 'sku', prefix: '' },
|
||||
{ type: 'entity', prefix: '' },
|
||||
{ type: 'delivery', prefix: '' },
|
||||
{ type: 'adjustment', prefix: '' },
|
||||
{ type: 'shipment', prefix: '-L-' },
|
||||
];
|
||||
|
||||
function shape(row) {
|
||||
return {
|
||||
@@ -30,7 +36,7 @@ export default function list(provider) {
|
||||
|
||||
/**
|
||||
* Lists the number sequences of the user's selected company.
|
||||
* Ensures that default sequences ('sku', 'entity', 'delivery') exist.
|
||||
* Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment') exist.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
@@ -44,8 +50,10 @@ export default function list(provider) {
|
||||
const { companyId } = await access(googleAuthId, tx);
|
||||
const runner = makeRunner(provider, tx);
|
||||
|
||||
for (const type of DEFAULT_TYPES) {
|
||||
await runner.query(ensureSql, { companyId, type });
|
||||
for (const item of DEFAULT_TYPES) {
|
||||
const type = typeof item === 'string' ? item : item.type;
|
||||
const prefix = typeof item === 'string' ? (type === 'shipment' ? '-L-' : '') : item.prefix;
|
||||
await runner.query(ensureSql, { companyId, type, prefix });
|
||||
}
|
||||
|
||||
const result = await runner.query(sql, { companyId });
|
||||
|
||||
@@ -37,7 +37,8 @@ export default function nextNumber(provider) {
|
||||
*/
|
||||
return async (companyId, type, tx) => {
|
||||
const doAlloc = async (runner) => {
|
||||
await runner.query(ensureSql, { companyId, type });
|
||||
const prefix = type === 'shipment' ? '-L-' : '';
|
||||
await runner.query(ensureSql, { companyId, type, prefix });
|
||||
const res = await runner.query(getSql, { companyId, type });
|
||||
const row = res.rows[0];
|
||||
if (!row) {
|
||||
|
||||
@@ -34,7 +34,8 @@ export default function peekNumber(provider) {
|
||||
*/
|
||||
return async (companyId, type, tx) => {
|
||||
const doPeek = async (runner) => {
|
||||
await runner.query(ensureSql, { companyId, type });
|
||||
const prefix = type === 'shipment' ? '-L-' : '';
|
||||
await runner.query(ensureSql, { companyId, type, prefix });
|
||||
const res = await runner.query(getSql, { companyId, type });
|
||||
const row = res.rows[0];
|
||||
if (!row) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||
import getOptionsByArticleSku from './getOptionsByArticleSku.js';
|
||||
import {
|
||||
recordOptionsListChangeHistory,
|
||||
recordArticleHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
async function hasOptionslistCycle(runner, rootArticleId, candidateOptionId) {
|
||||
@@ -44,7 +46,6 @@ export default function setOptionsByArticleSku(provider) {
|
||||
const getOptions = getOptionsByArticleSku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Sets or clears the options list for an article.
|
||||
@@ -100,10 +101,11 @@ export default function setOptionsByArticleSku(provider) {
|
||||
});
|
||||
|
||||
if (oldOptNames.length) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordArticleHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Optionsliste geleert (war: [${oldOptNames.join(', ')}])`,
|
||||
type: 'optionslist_cleared',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -228,11 +230,10 @@ export default function setOptionsByArticleSku(provider) {
|
||||
const result = await getOptions(googleAuthId, sku, runner);
|
||||
|
||||
// Write history for options list change
|
||||
const newNames = result.map((r) => r.name || r.sku);
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordOptionsListChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Optionsliste gesetzt: [${newNames.join(', ')}]`,
|
||||
options: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||
import {
|
||||
FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES,
|
||||
INSERT_ARTICLE_HISTORY as MSSQL_INSERT_HISTORY,
|
||||
} from '../../providers/mssql/queries/ledger.js';
|
||||
import {
|
||||
FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES,
|
||||
INSERT_ARTICLE_HISTORY as SQLITE_INSERT_HISTORY,
|
||||
} from '../../providers/sqlite3/queries/ledger.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js';
|
||||
import { recordOrderHistory } from '../articlehistory/record.js';
|
||||
import { syncOrderSummary } from './syncSummary.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function create(provider) {
|
||||
@@ -28,7 +28,6 @@ export default function create(provider) {
|
||||
const createManifestSql = isSqlite ? SQLITE_CREATE_MANIFEST : MSSQL_CREATE_MANIFEST;
|
||||
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
|
||||
const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Creates a new order for the user's selected company with its lines and records article history.
|
||||
@@ -98,6 +97,8 @@ export default function create(provider) {
|
||||
customerName = entRes.rows[0]?.name ?? null;
|
||||
}
|
||||
|
||||
const historyDate = body.createdAt ? createdAt : date;
|
||||
|
||||
for (const line of lines) {
|
||||
const sku = line.sku ? String(line.sku).trim() : '';
|
||||
if (!sku) continue;
|
||||
@@ -107,21 +108,18 @@ export default function create(provider) {
|
||||
companyId,
|
||||
});
|
||||
|
||||
const details = [];
|
||||
if (customerName) {
|
||||
details.push(`Kunde: "${customerName}"`);
|
||||
}
|
||||
if (line.amount != null && Number.isFinite(Number(line.amount))) {
|
||||
details.push(`Menge: ${line.amount}`);
|
||||
}
|
||||
const suffix = details.length ? ` (${details.join(', ')})` : '';
|
||||
const historyText = `Auftrag ${number}${suffix}`;
|
||||
|
||||
for (const match of articleMatches.rows) {
|
||||
await runner.query(insertHistorySql, {
|
||||
await recordOrderHistory(runner, {
|
||||
articleId: match.articleId,
|
||||
companyuserId,
|
||||
text: historyText,
|
||||
date: historyDate,
|
||||
orderId: manifestId,
|
||||
number,
|
||||
customerName,
|
||||
amount: line.amount,
|
||||
net: line.net,
|
||||
rebate: line.rebate,
|
||||
taxrate: line.taxrate,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -132,6 +130,8 @@ export default function create(provider) {
|
||||
id: manifestId,
|
||||
});
|
||||
|
||||
await syncOrderSummary(runner, { companyId, manifestId });
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
COUNT_MANIFESTS as MSSQL_COUNT,
|
||||
GET_MANIFEST_LINES_BY_MANIFEST_IDS as MSSQL_LINES_BY_MANIFEST_IDS,
|
||||
LIST_MANIFESTS as MSSQL_LIST,
|
||||
LIST_MANIFESTS_PAGED as MSSQL_LIST_PAGED,
|
||||
ORDER_TYPE as MSSQL_ORDER_TYPE,
|
||||
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||
import {
|
||||
COUNT_MANIFESTS as SQLITE_COUNT,
|
||||
GET_MANIFEST_LINES_BY_MANIFEST_IDS as SQLITE_LINES_BY_MANIFEST_IDS,
|
||||
LIST_MANIFESTS as SQLITE_LIST,
|
||||
LIST_MANIFESTS_PAGED as SQLITE_LIST_PAGED,
|
||||
ORDER_TYPE as SQLITE_ORDER_TYPE,
|
||||
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||
import { GET_SHIPMENT_PREFIX as MSSQL_GET_SHIPMENT_PREFIX } from '../../providers/mssql/queries/shipment.js';
|
||||
import { GET_SHIPMENT_PREFIX as SQLITE_GET_SHIPMENT_PREFIX } from '../../providers/sqlite3/queries/shipment.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { shapeSummary } from '../manifestDoc/helpers.js';
|
||||
import { shapeLine } from '../manifestLines.js';
|
||||
@@ -20,27 +16,31 @@ import makeRunner from '../runner.js';
|
||||
export default function list(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const sql = isSqlite ? SQLITE_LIST : MSSQL_LIST;
|
||||
const sqlCount = isSqlite ? SQLITE_COUNT : MSSQL_COUNT;
|
||||
const sqlPaged = isSqlite ? SQLITE_LIST_PAGED : MSSQL_LIST_PAGED;
|
||||
const sqlLinesByManifestIds = isSqlite
|
||||
? SQLITE_LINES_BY_MANIFEST_IDS
|
||||
: MSSQL_LINES_BY_MANIFEST_IDS;
|
||||
const sqlShipmentPrefix = isSqlite
|
||||
? SQLITE_GET_SHIPMENT_PREFIX
|
||||
: MSSQL_GET_SHIPMENT_PREFIX;
|
||||
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
|
||||
|
||||
/**
|
||||
* Lists the orders of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [opts] - Optional pagination options.
|
||||
* @param {object} [opts] - Optional pagination and filter options.
|
||||
* @param {number} [opts.offset=0] - Number of orders to skip.
|
||||
* @param {number} [opts.limit=20] - Number of orders to return.
|
||||
* @param {number} [opts.limit=50] - Number of orders to return.
|
||||
* @param {string} [opts.search] - Free-text search matching order number, external number, or customer name.
|
||||
* @param {boolean} [opts.onlyUnpaid=false] - When true, returns only unpaid orders.
|
||||
* @param {boolean} [opts.onlyUndelivered=false] - When true, returns only undelivered orders.
|
||||
* @param {number} [opts.seekMonths] - Negative integer for cutoff months ago (e.g. -1).
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, offset: number, limit: number}|Array<object>>} The orders.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.order.list(42, { offset: 0, limit: 20 });
|
||||
* // => { orders: [...], total: 100, offset: 0, limit: 20 }
|
||||
* const result = await store.order.list(42, { offset: 0, limit: 50, search: '1002', onlyUnpaid: true, onlyUndelivered: true, seekMonths: -1 });
|
||||
* // => { orders: [...], total: 10, offset: 0, limit: 50 }
|
||||
*
|
||||
* const orders = await store.order.list(42);
|
||||
* // => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }]
|
||||
@@ -51,11 +51,20 @@ export default function list(provider) {
|
||||
let isPaged = false;
|
||||
let resolvedTx = tx;
|
||||
|
||||
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
|
||||
opts.offset !== undefined ||
|
||||
opts.limit !== undefined ||
|
||||
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
|
||||
opts.onlyUnpaid === true ||
|
||||
opts.onlyUndelivered === true ||
|
||||
opts.seekMonths !== undefined
|
||||
);
|
||||
|
||||
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||
if (opts.offset !== undefined || opts.limit !== undefined) {
|
||||
if (hasFilter || opts.offset !== undefined || opts.limit !== undefined) {
|
||||
isPaged = true;
|
||||
const parsedOffset = Number(opts.offset ?? 0);
|
||||
const parsedLimit = Number(opts.limit ?? 20);
|
||||
const parsedLimit = Number(opts.limit ?? 50);
|
||||
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
|
||||
throw new Error('Invalid offset');
|
||||
}
|
||||
@@ -72,18 +81,133 @@ export default function list(provider) {
|
||||
const runner = makeRunner(provider, resolvedTx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const conditions = ['m.company_id = @companyId', `m.${isSqlite ? '"type"' : '[type]'} = @type`];
|
||||
const params = {
|
||||
companyId,
|
||||
type: orderType,
|
||||
};
|
||||
|
||||
// Date cutoff via seekMonths (strictly integer)
|
||||
if (opts?.seekMonths !== undefined) {
|
||||
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
|
||||
throw new Error('Invalid seekMonths');
|
||||
}
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
|
||||
params.dateTo = d.toISOString();
|
||||
conditions.push(`m.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
|
||||
}
|
||||
|
||||
// Free-text search (strictly string)
|
||||
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
|
||||
params.searchPattern = `%${opts.search.trim()}%`;
|
||||
if (isSqlite) {
|
||||
conditions.push(`(
|
||||
m."number" LIKE @searchPattern
|
||||
OR m.external_number LIKE @searchPattern
|
||||
OR e.name LIKE @searchPattern
|
||||
OR e.first_name LIKE @searchPattern
|
||||
OR (IFNULL(e.first_name, '') || ' ' || IFNULL(e.name, '')) LIKE @searchPattern
|
||||
OR (IFNULL(e.name, '') || ' ' || IFNULL(e.first_name, '')) LIKE @searchPattern
|
||||
)`);
|
||||
} else {
|
||||
conditions.push(`(
|
||||
m.[number] LIKE @searchPattern
|
||||
OR m.external_number LIKE @searchPattern
|
||||
OR e.name LIKE @searchPattern
|
||||
OR e.first_name LIKE @searchPattern
|
||||
OR (ISNULL(e.first_name, '') + ' ' + ISNULL(e.name, '')) LIKE @searchPattern
|
||||
OR (ISNULL(e.name, '') + ' ' + ISNULL(e.first_name, '')) LIKE @searchPattern
|
||||
)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only unpaid (strictly boolean true; overpaid orders also count as unpaid)
|
||||
if (opts?.onlyUnpaid === true) {
|
||||
conditions.push('COALESCE(os.is_paid, 0) = 0');
|
||||
}
|
||||
|
||||
// Only undelivered (strictly boolean true)
|
||||
if (opts?.onlyUndelivered === true) {
|
||||
conditions.push('COALESCE(os.is_shipped, 0) = 0');
|
||||
}
|
||||
|
||||
const whereClause = conditions.join('\n AND ');
|
||||
const sqlCount = `
|
||||
SELECT COUNT(*) AS total
|
||||
FROM manifest m
|
||||
LEFT JOIN ordersummary os ON os.manifest_id = m.id
|
||||
LEFT JOIN entity e ON e.id = m.entity_id
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
let sqlList;
|
||||
if (isSqlite) {
|
||||
sqlList = `
|
||||
SELECT
|
||||
m.id,
|
||||
m."number",
|
||||
m."type",
|
||||
m.entity_id AS entityId,
|
||||
m.external_number AS externalNumber,
|
||||
m."date",
|
||||
m.created_at AS createdAt,
|
||||
os.positions,
|
||||
os.gross_total AS grossTotal,
|
||||
os.paid_total AS paidTotal,
|
||||
os.ordered_quantity AS orderedQuantity,
|
||||
os.shipped_quantity AS shippedQuantity,
|
||||
os.is_paid AS isPaid,
|
||||
os.is_shipped AS isShipped,
|
||||
os.customer_name AS customerName
|
||||
FROM manifest m
|
||||
LEFT JOIN ordersummary os ON os.manifest_id = m.id
|
||||
LEFT JOIN entity e ON e.id = m.entity_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY m."date" DESC, m.id DESC
|
||||
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
|
||||
`;
|
||||
} else {
|
||||
sqlList = `
|
||||
SELECT
|
||||
m.id,
|
||||
m.[number],
|
||||
m.[type],
|
||||
m.entity_id AS entityId,
|
||||
m.external_number AS externalNumber,
|
||||
m.[date],
|
||||
m.created_at AS createdAt,
|
||||
os.positions,
|
||||
os.gross_total AS grossTotal,
|
||||
os.paid_total AS paidTotal,
|
||||
os.ordered_quantity AS orderedQuantity,
|
||||
os.shipped_quantity AS shippedQuantity,
|
||||
os.is_paid AS isPaid,
|
||||
os.is_shipped AS isShipped,
|
||||
os.customer_name AS customerName
|
||||
FROM manifest m
|
||||
LEFT JOIN ordersummary os ON os.manifest_id = m.id
|
||||
LEFT JOIN entity e ON e.id = m.entity_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY m.[date] DESC, m.id DESC
|
||||
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
let manifestRows;
|
||||
let total = 0;
|
||||
|
||||
if (isPaged) {
|
||||
params.offset = offset;
|
||||
params.limit = limit;
|
||||
const [countResult, listResult] = await Promise.all([
|
||||
runner.query(sqlCount, { companyId, type: orderType }),
|
||||
runner.query(sqlPaged, { companyId, type: orderType, offset, limit }),
|
||||
runner.query(sqlCount, params),
|
||||
runner.query(sqlList, params),
|
||||
]);
|
||||
total = Number(countResult.rows[0]?.total ?? 0);
|
||||
manifestRows = listResult.rows;
|
||||
} else {
|
||||
const result = await runner.query(sql, { companyId, type: orderType });
|
||||
const result = await runner.query(sqlList, params);
|
||||
manifestRows = result.rows;
|
||||
total = manifestRows.length;
|
||||
}
|
||||
@@ -108,6 +232,14 @@ export default function list(provider) {
|
||||
const orders = manifestRows.map((row) => ({
|
||||
...shapeSummary(orderType, row),
|
||||
lines: linesByManifestId.get(row.id) ?? [],
|
||||
positions: row.positions !== undefined && row.positions !== null ? Number(row.positions) : undefined,
|
||||
grossTotal: row.grossTotal !== undefined && row.grossTotal !== null ? Number(row.grossTotal) : undefined,
|
||||
paidTotal: row.paidTotal !== undefined && row.paidTotal !== null ? Number(row.paidTotal) : undefined,
|
||||
orderedQuantity: row.orderedQuantity !== undefined && row.orderedQuantity !== null ? Number(row.orderedQuantity) : undefined,
|
||||
shippedQuantity: row.shippedQuantity !== undefined && row.shippedQuantity !== null ? Number(row.shippedQuantity) : undefined,
|
||||
isPaid: row.isPaid !== undefined && row.isPaid !== null ? Boolean(row.isPaid) : undefined,
|
||||
isShipped: row.isShipped !== undefined && row.isShipped !== null ? Boolean(row.isShipped) : undefined,
|
||||
customerName: row.customerName ?? undefined,
|
||||
}));
|
||||
|
||||
if (isPaged) {
|
||||
@@ -122,5 +254,3 @@ export default function list(provider) {
|
||||
return orders;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
19
src/store/order/rebuildSummaries.js
Normal file
19
src/store/order/rebuildSummaries.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { rebuildOrderSummaries } from './syncSummary.js';
|
||||
|
||||
/**
|
||||
* Rebuilds the `ordersummary` table for the user's selected company.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||
*/
|
||||
export default function rebuildSummaries(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
|
||||
return async (googleAuthId, tx) => {
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
await rebuildOrderSummaries(runner, companyId);
|
||||
return { ok: true, companyId };
|
||||
};
|
||||
}
|
||||
364
src/store/order/syncSummary.js
Normal file
364
src/store/order/syncSummary.js
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Centralized sync and rebuild logic for the materialized `ordersummary` table.
|
||||
*/
|
||||
|
||||
const SQL_GET_SHIPMENT_PREFIX = `
|
||||
SELECT prefix FROM numbers WHERE company_id = @companyId AND [type] = 'shipment'
|
||||
`;
|
||||
|
||||
const SQL_GET_ORDER_BY_ID_SQLITE = `
|
||||
SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id
|
||||
FROM manifest m
|
||||
WHERE m.id = @manifestId AND m.company_id = @companyId AND m."type" = 'order'
|
||||
`;
|
||||
|
||||
const SQL_GET_ORDER_BY_ID_MSSQL = `
|
||||
SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id
|
||||
FROM manifest m
|
||||
WHERE m.id = @manifestId AND m.company_id = @companyId AND m.[type] = 'order'
|
||||
`;
|
||||
|
||||
const SQL_GET_ORDER_BY_NUMBER_SQLITE = `
|
||||
SELECT m.id, m.company_id, m."number", m."date", m.created_at, m.entity_id
|
||||
FROM manifest m
|
||||
WHERE m."number" = @number AND m.company_id = @companyId AND m."type" = 'order'
|
||||
`;
|
||||
|
||||
const SQL_GET_ORDER_BY_NUMBER_MSSQL = `
|
||||
SELECT m.id, m.company_id, m.[number], m.[date], m.created_at, m.entity_id
|
||||
FROM manifest m
|
||||
WHERE m.[number] = @number AND m.company_id = @companyId AND m.[type] = 'order'
|
||||
`;
|
||||
|
||||
const SQL_LINE_TOTALS = `
|
||||
SELECT
|
||||
COUNT(*) AS positions,
|
||||
COALESCE(SUM(amount), 0) AS ordered_quantity,
|
||||
COALESCE(SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)), 0) AS gross_total
|
||||
FROM manifestarticle
|
||||
WHERE manifest_id = @manifestId
|
||||
`;
|
||||
|
||||
const SQL_PAYMENT_TOTAL = `
|
||||
SELECT COALESCE(SUM(amount), 0) AS paid_total
|
||||
FROM payment
|
||||
WHERE manifest_id = @manifestId
|
||||
`;
|
||||
|
||||
const SQL_SHIPPED_QTY_SQLITE = `
|
||||
SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity
|
||||
FROM manifest m_s
|
||||
JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id
|
||||
WHERE m_s.company_id = @companyId
|
||||
AND m_s."type" = 'shipment'
|
||||
AND m_s."number" >= (@orderNumber || @shipmentPrefix)
|
||||
AND m_s."number" < (@orderNumber || @shipmentPrefix || char(127))
|
||||
`;
|
||||
|
||||
const SQL_SHIPPED_QTY_MSSQL = `
|
||||
SELECT COALESCE(SUM(ma_s.amount), 0) AS shipped_quantity
|
||||
FROM manifest m_s
|
||||
JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id
|
||||
WHERE m_s.company_id = @companyId
|
||||
AND m_s.[type] = 'shipment'
|
||||
AND m_s.[number] >= (@orderNumber + @shipmentPrefix)
|
||||
AND m_s.[number] < (@orderNumber + @shipmentPrefix + NCHAR(127))
|
||||
`;
|
||||
|
||||
const SQL_CUSTOMER_NAME_SQLITE = `
|
||||
SELECT TRIM(COALESCE(first_name || ' ', '') || COALESCE(name, '')) AS customer_name
|
||||
FROM entity
|
||||
WHERE id = @entityId
|
||||
`;
|
||||
|
||||
const SQL_CUSTOMER_NAME_MSSQL = `
|
||||
SELECT LTRIM(RTRIM(ISNULL(first_name + ' ', '') + ISNULL(name, ''))) AS customer_name
|
||||
FROM entity
|
||||
WHERE id = @entityId
|
||||
`;
|
||||
|
||||
const SQL_UPSERT_SUMMARY_SQLITE = `
|
||||
INSERT INTO ordersummary (
|
||||
company_id, manifest_id, order_number, order_date, created_at,
|
||||
entity_id, customer_name, positions, gross_total, paid_total,
|
||||
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||
) VALUES (
|
||||
@companyId, @manifestId, @orderNumber, @orderDate, @createdAt,
|
||||
@entityId, @customerName, @positions, @grossTotal, @paidTotal,
|
||||
@orderedQuantity, @shippedQuantity, @isPaid, @isShipped
|
||||
)
|
||||
ON CONFLICT(manifest_id) DO UPDATE SET
|
||||
order_number = excluded.order_number,
|
||||
order_date = excluded.order_date,
|
||||
created_at = excluded.created_at,
|
||||
entity_id = excluded.entity_id,
|
||||
customer_name = excluded.customer_name,
|
||||
positions = excluded.positions,
|
||||
gross_total = excluded.gross_total,
|
||||
paid_total = excluded.paid_total,
|
||||
ordered_quantity = excluded.ordered_quantity,
|
||||
shipped_quantity = excluded.shipped_quantity,
|
||||
is_paid = excluded.is_paid,
|
||||
is_shipped = excluded.is_shipped
|
||||
`;
|
||||
|
||||
const SQL_UPSERT_SUMMARY_MSSQL = `
|
||||
MERGE INTO ordersummary AS target
|
||||
USING (SELECT @manifestId AS manifest_id) AS source
|
||||
ON (target.manifest_id = source.manifest_id)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET
|
||||
order_number = @orderNumber,
|
||||
order_date = @orderDate,
|
||||
created_at = @createdAt,
|
||||
entity_id = @entityId,
|
||||
customer_name = @customerName,
|
||||
positions = @positions,
|
||||
gross_total = @grossTotal,
|
||||
paid_total = @paidTotal,
|
||||
ordered_quantity = @orderedQuantity,
|
||||
shipped_quantity = @shippedQuantity,
|
||||
is_paid = @isPaid,
|
||||
is_shipped = @isShipped
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (
|
||||
company_id, manifest_id, order_number, order_date, created_at,
|
||||
entity_id, customer_name, positions, gross_total, paid_total,
|
||||
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||
) VALUES (
|
||||
@companyId, @manifestId, @orderNumber, @orderDate, @createdAt,
|
||||
@entityId, @customerName, @positions, @grossTotal, @paidTotal,
|
||||
@orderedQuantity, @shippedQuantity, @isPaid, @isShipped
|
||||
);
|
||||
`;
|
||||
|
||||
/**
|
||||
* Recalculates and upserts a single order summary into `ordersummary`.
|
||||
*
|
||||
* @param {object} runner - Database runner.
|
||||
* @param {object} opts
|
||||
* @param {number} opts.companyId
|
||||
* @param {number} [opts.manifestId] - Order manifest id.
|
||||
* @param {string} [opts.orderNumber] - Order number (if manifestId not given).
|
||||
*/
|
||||
export async function syncOrderSummary(runner, { companyId, manifestId, orderNumber }) {
|
||||
const isSqlite = runner.dialect === 'sqlite3';
|
||||
|
||||
let orderRow;
|
||||
if (manifestId) {
|
||||
const res = await runner.query(
|
||||
isSqlite ? SQL_GET_ORDER_BY_ID_SQLITE : SQL_GET_ORDER_BY_ID_MSSQL,
|
||||
{ manifestId, companyId },
|
||||
);
|
||||
orderRow = res.rows[0];
|
||||
} else if (orderNumber) {
|
||||
const res = await runner.query(
|
||||
isSqlite ? SQL_GET_ORDER_BY_NUMBER_SQLITE : SQL_GET_ORDER_BY_NUMBER_MSSQL,
|
||||
{ number: orderNumber, companyId },
|
||||
);
|
||||
orderRow = res.rows[0];
|
||||
}
|
||||
|
||||
if (!orderRow) {
|
||||
if (manifestId) {
|
||||
await runner.query('DELETE FROM ordersummary WHERE manifest_id = @manifestId', { manifestId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const orderId = orderRow.id;
|
||||
const num = orderRow.number;
|
||||
|
||||
const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId });
|
||||
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||
|
||||
const [lineRes, payRes, shipRes] = await Promise.all([
|
||||
runner.query(SQL_LINE_TOTALS, { manifestId: orderId }),
|
||||
runner.query(SQL_PAYMENT_TOTAL, { manifestId: orderId }),
|
||||
runner.query(isSqlite ? SQL_SHIPPED_QTY_SQLITE : SQL_SHIPPED_QTY_MSSQL, {
|
||||
companyId,
|
||||
orderNumber: num,
|
||||
shipmentPrefix,
|
||||
}),
|
||||
]);
|
||||
|
||||
let customerName = null;
|
||||
if (orderRow.entity_id) {
|
||||
const custRes = await runner.query(
|
||||
isSqlite ? SQL_CUSTOMER_NAME_SQLITE : SQL_CUSTOMER_NAME_MSSQL,
|
||||
{ entityId: orderRow.entity_id },
|
||||
);
|
||||
customerName = custRes.rows[0]?.customer_name || null;
|
||||
}
|
||||
|
||||
const positions = Number(lineRes.rows[0]?.positions || 0);
|
||||
const orderedQuantity = Math.round(Number(lineRes.rows[0]?.ordered_quantity || 0) * 1000) / 1000;
|
||||
const grossTotal = Math.round(Number(lineRes.rows[0]?.gross_total || 0) * 100) / 100;
|
||||
const paidTotal = Math.round(Number(payRes.rows[0]?.paid_total || 0) * 100) / 100;
|
||||
const shippedQuantity = Math.round(Number(shipRes.rows[0]?.shipped_quantity || 0) * 1000) / 1000;
|
||||
|
||||
const isPaid = grossTotal > 0 && Math.abs(paidTotal - grossTotal) < 0.01 ? 1 : 0;
|
||||
const isShipped = orderedQuantity > 0 && Math.abs(shippedQuantity - orderedQuantity) < 0.001 ? 1 : 0;
|
||||
|
||||
await runner.query(isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL, {
|
||||
companyId,
|
||||
manifestId: orderId,
|
||||
orderNumber: num,
|
||||
orderDate: orderRow.date,
|
||||
createdAt: orderRow.created_at,
|
||||
entityId: orderRow.entity_id ?? null,
|
||||
customerName,
|
||||
positions,
|
||||
grossTotal,
|
||||
paidTotal,
|
||||
orderedQuantity,
|
||||
shippedQuantity,
|
||||
isPaid,
|
||||
isShipped,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Full rebuild of all orders in `ordersummary` for a given company.
|
||||
*
|
||||
* @param {object} runner
|
||||
* @param {number} companyId
|
||||
*/
|
||||
export async function rebuildOrderSummaries(runner, companyId) {
|
||||
const isSqlite = runner.dialect === 'sqlite3';
|
||||
|
||||
const prefixRes = await runner.query(SQL_GET_SHIPMENT_PREFIX, { companyId });
|
||||
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||
|
||||
if (isSqlite) {
|
||||
const sqlRebuildSqlite = `
|
||||
WITH
|
||||
order_articles AS (
|
||||
SELECT
|
||||
manifest_id,
|
||||
COUNT(*) AS positions,
|
||||
SUM(amount) AS ordered_quantity,
|
||||
SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total
|
||||
FROM manifestarticle
|
||||
GROUP BY manifest_id
|
||||
),
|
||||
order_payments AS (
|
||||
SELECT
|
||||
manifest_id,
|
||||
SUM(amount) AS paid_total
|
||||
FROM payment
|
||||
GROUP BY manifest_id
|
||||
),
|
||||
shipment_totals AS (
|
||||
SELECT
|
||||
SUBSTR(m_s."number", 1, INSTR(m_s."number", @shipmentPrefix) - 1) AS order_number,
|
||||
SUM(ma_s.amount) AS shipped_quantity
|
||||
FROM manifest m_s
|
||||
JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id
|
||||
WHERE m_s.company_id = @companyId
|
||||
AND m_s."type" = 'shipment'
|
||||
AND INSTR(m_s."number", @shipmentPrefix) > 0
|
||||
GROUP BY SUBSTR(m_s."number", 1, INSTR(m_s."number", @shipmentPrefix) - 1)
|
||||
)
|
||||
INSERT INTO ordersummary (
|
||||
company_id, manifest_id, order_number, order_date, created_at,
|
||||
entity_id, customer_name, positions, gross_total, paid_total,
|
||||
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||
)
|
||||
SELECT
|
||||
m.company_id,
|
||||
m.id AS manifest_id,
|
||||
m."number" AS order_number,
|
||||
m."date" AS order_date,
|
||||
m.created_at,
|
||||
m.entity_id,
|
||||
TRIM(COALESCE(e.first_name || ' ', '') || COALESCE(e.name, '')) AS customer_name,
|
||||
COALESCE(oa.positions, 0) AS positions,
|
||||
ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total,
|
||||
ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total,
|
||||
ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity,
|
||||
ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity,
|
||||
CASE WHEN COALESCE(oa.gross_total, 0) > 0 AND ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid,
|
||||
CASE WHEN COALESCE(oa.ordered_quantity, 0) > 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped
|
||||
FROM manifest m
|
||||
LEFT JOIN entity e ON e.id = m.entity_id
|
||||
LEFT JOIN order_articles oa ON oa.manifest_id = m.id
|
||||
LEFT JOIN order_payments op ON op.manifest_id = m.id
|
||||
LEFT JOIN shipment_totals st ON st.order_number = m."number"
|
||||
WHERE m.company_id = @companyId AND m."type" = 'order'
|
||||
ON CONFLICT(manifest_id) DO UPDATE SET
|
||||
order_number = excluded.order_number,
|
||||
order_date = excluded.order_date,
|
||||
created_at = excluded.created_at,
|
||||
entity_id = excluded.entity_id,
|
||||
customer_name = excluded.customer_name,
|
||||
positions = excluded.positions,
|
||||
gross_total = excluded.gross_total,
|
||||
paid_total = excluded.paid_total,
|
||||
ordered_quantity = excluded.ordered_quantity,
|
||||
shipped_quantity = excluded.shipped_quantity,
|
||||
is_paid = excluded.is_paid,
|
||||
is_shipped = excluded.is_shipped;
|
||||
`;
|
||||
await runner.query(sqlRebuildSqlite, { companyId, shipmentPrefix });
|
||||
} else {
|
||||
await runner.query('DELETE FROM ordersummary WHERE company_id = @companyId', { companyId });
|
||||
const sqlRebuildMssql = `
|
||||
WITH
|
||||
order_articles AS (
|
||||
SELECT
|
||||
manifest_id,
|
||||
COUNT(*) AS positions,
|
||||
SUM(amount) AS ordered_quantity,
|
||||
SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total
|
||||
FROM manifestarticle
|
||||
GROUP BY manifest_id
|
||||
),
|
||||
order_payments AS (
|
||||
SELECT
|
||||
manifest_id,
|
||||
SUM(amount) AS paid_total
|
||||
FROM payment
|
||||
GROUP BY manifest_id
|
||||
),
|
||||
shipment_totals AS (
|
||||
SELECT
|
||||
SUBSTRING(m_s.[number], 1, CHARINDEX(@shipmentPrefix, m_s.[number]) - 1) AS order_number,
|
||||
SUM(ma_s.amount) AS shipped_quantity
|
||||
FROM manifest m_s
|
||||
JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id
|
||||
WHERE m_s.company_id = @companyId
|
||||
AND m_s.[type] = 'shipment'
|
||||
AND CHARINDEX(@shipmentPrefix, m_s.[number]) > 0
|
||||
GROUP BY SUBSTRING(m_s.[number], 1, CHARINDEX(@shipmentPrefix, m_s.[number]) - 1)
|
||||
)
|
||||
INSERT INTO ordersummary (
|
||||
company_id, manifest_id, order_number, order_date, created_at,
|
||||
entity_id, customer_name, positions, gross_total, paid_total,
|
||||
ordered_quantity, shipped_quantity, is_paid, is_shipped
|
||||
)
|
||||
SELECT
|
||||
m.company_id,
|
||||
m.id AS manifest_id,
|
||||
m.[number] AS order_number,
|
||||
m.[date] AS order_date,
|
||||
m.created_at,
|
||||
m.entity_id,
|
||||
LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, ''))) AS customer_name,
|
||||
COALESCE(oa.positions, 0) AS positions,
|
||||
ROUND(COALESCE(oa.gross_total, 0), 2) AS gross_total,
|
||||
ROUND(COALESCE(op.paid_total, 0), 2) AS paid_total,
|
||||
ROUND(COALESCE(oa.ordered_quantity, 0), 3) AS ordered_quantity,
|
||||
ROUND(COALESCE(st.shipped_quantity, 0), 3) AS shipped_quantity,
|
||||
CASE WHEN COALESCE(oa.gross_total, 0) > 0 AND ABS(COALESCE(op.paid_total, 0) - oa.gross_total) < 0.01 THEN 1 ELSE 0 END AS is_paid,
|
||||
CASE WHEN COALESCE(oa.ordered_quantity, 0) > 0 AND ABS(COALESCE(st.shipped_quantity, 0) - oa.ordered_quantity) < 0.001 THEN 1 ELSE 0 END AS is_shipped
|
||||
FROM manifest m
|
||||
LEFT JOIN entity e ON e.id = m.entity_id
|
||||
LEFT JOIN order_articles oa ON oa.manifest_id = m.id
|
||||
LEFT JOIN order_payments op ON op.manifest_id = m.id
|
||||
LEFT JOIN shipment_totals st ON st.order_number = m.[number]
|
||||
WHERE m.company_id = @companyId AND m.[type] = 'order';
|
||||
`;
|
||||
await runner.query(sqlRebuildMssql, { companyId, shipmentPrefix });
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js';
|
||||
import { syncOrderSummary } from './syncSummary.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function update(provider) {
|
||||
@@ -146,6 +147,7 @@ export default function update(provider) {
|
||||
}
|
||||
|
||||
const doc = await loadDoc(runner, { type: orderType, companyId, id });
|
||||
await syncOrderSummary(runner, { companyId, manifestId: id });
|
||||
return doc;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { queriesFor, requireId, requireText } from '../logistics/helpers.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import resolveManifestAccess from '../manifestattachment/resolveManifestAccess.js';
|
||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
function shape(row) {
|
||||
@@ -64,6 +65,7 @@ export default function create(provider) {
|
||||
type,
|
||||
amount: input.amount,
|
||||
};
|
||||
await syncOrderSummary(runner, { companyId, manifestId });
|
||||
return shape(row);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { queriesFor, requireId } from '../logistics/helpers.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
/**
|
||||
@@ -21,6 +22,17 @@ export default function del(provider) {
|
||||
const paymentId = requireId(id, 'Payment');
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const paymentRow = await runner.query(
|
||||
'SELECT manifest_id FROM payment WHERE id = @id',
|
||||
{ id: paymentId },
|
||||
);
|
||||
const manifestId = paymentRow.rows[0]?.manifest_id;
|
||||
|
||||
await runner.query(q.PAYMENT_DELETE, { id: paymentId, companyId });
|
||||
|
||||
if (manifestId) {
|
||||
await syncOrderSummary(runner, { companyId, manifestId });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,21 +12,28 @@
|
||||
* @returns {((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }}
|
||||
*/
|
||||
|
||||
import { logSlowQuery } from '../providers/provider.js';
|
||||
|
||||
/** Wraps a raw mssql pool/transaction in the provider query contract. */
|
||||
function wrapMssql(target) {
|
||||
return {
|
||||
dialect: 'mssql',
|
||||
async query(sql, params = {}) {
|
||||
const req = target.request();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
req.input(key, value);
|
||||
const start = performance.now();
|
||||
try {
|
||||
const req = target.request();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
req.input(key, value);
|
||||
}
|
||||
const result = await req.query(sql);
|
||||
const rows = result.recordset ?? [];
|
||||
const rowsAffected = Array.isArray(result.rowsAffected)
|
||||
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
||||
: 0;
|
||||
return { rows, rowsAffected, insertId: null };
|
||||
} finally {
|
||||
logSlowQuery(sql, performance.now() - start, params);
|
||||
}
|
||||
const result = await req.query(sql);
|
||||
const rows = result.recordset ?? [];
|
||||
const rowsAffected = Array.isArray(result.rowsAffected)
|
||||
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
||||
: 0;
|
||||
return { rows, rowsAffected, insertId: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
198
src/store/shipment/create.js
Normal file
198
src/store/shipment/create.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, parseDate } from '../manifestLines.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { loadShipment, queriesFor } from './helpers.js';
|
||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||
|
||||
function requireId(val, name) {
|
||||
const n = Number(val);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shipment manifest for an order.
|
||||
* Validates that all article positions belong to the order and do not exceed remaining ordered quantities.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||
*/
|
||||
export default function create(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const ensureTax = taxEnsure(provider);
|
||||
const q = queriesFor(provider);
|
||||
|
||||
/**
|
||||
* Creates a shipment manifest for an order.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The shipment input.
|
||||
* @param {number} input.orderId - The parent order manifest id.
|
||||
* @param {string|Date} [input.date] - Optional shipment date (defaults to current date).
|
||||
* @param {string} [input.externalNumber] - Optional external delivery note number.
|
||||
* @param {Array<{sku: string, amount: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>} input.lines - The shipment positions.
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<object>} The created shipment manifest.
|
||||
* @throws {Error} If order not found, invalid lines, or article ceiling invariant exceeded.
|
||||
*
|
||||
* @example
|
||||
* const shipment = await store.shipment.create(googleAuthId, {
|
||||
* orderId: 10,
|
||||
* lines: [{ sku: 'ITEM-A', amount: 5 }]
|
||||
* });
|
||||
*/
|
||||
return async (googleAuthId, input = {}, tx) => {
|
||||
const orderId = requireId(input.orderId, 'Order');
|
||||
|
||||
if (!Array.isArray(input.lines) || input.lines.length === 0) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
const shipmentDate = parseDate(input.date) || new Date();
|
||||
const externalNumber = input.externalNumber?.trim?.()
|
||||
? input.externalNumber.trim()
|
||||
: null;
|
||||
|
||||
const run = async (runner, innerTx) => {
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
// 1. Retrieve parent order header
|
||||
const orderRes = await runner.query(q.GET_ORDER_HEADER, {
|
||||
orderId,
|
||||
companyId,
|
||||
});
|
||||
const order = orderRes.rows[0];
|
||||
if (!order) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
// 2. Retrieve parent order lines
|
||||
const orderLinesRes = await runner.query(q.GET_ORDER_LINES, {
|
||||
orderId,
|
||||
});
|
||||
const orderLines = orderLinesRes.rows ?? [];
|
||||
const orderAmountBySku = new Map();
|
||||
const orderLineBySku = new Map();
|
||||
|
||||
for (const ol of orderLines) {
|
||||
const sku = String(ol.sku ?? '').trim();
|
||||
if (!sku) continue;
|
||||
orderAmountBySku.set(sku, (orderAmountBySku.get(sku) || 0) + Number(ol.amount || 0));
|
||||
if (!orderLineBySku.has(sku)) {
|
||||
orderLineBySku.set(sku, ol);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validate proposed lines SKUs and amounts
|
||||
const proposedBySku = new Map();
|
||||
for (const line of input.lines) {
|
||||
const sku = String(line?.sku ?? '').trim();
|
||||
const amount = Number(line?.amount);
|
||||
|
||||
if (!sku || !orderAmountBySku.has(sku)) {
|
||||
throw new Error(`Article (${sku}) is not part of order (${order.number})`);
|
||||
}
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
proposedBySku.set(sku, (proposedBySku.get(sku) || 0) + amount);
|
||||
}
|
||||
|
||||
// 4. Retrieve shipment prefix from numbers table
|
||||
const prefixRes = await runner.query(q.GET_SHIPMENT_PREFIX, { companyId });
|
||||
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||
|
||||
// 5. Retrieve already shipped amounts across existing shipment manifests of this order
|
||||
const pattern = `${order.number}${shipmentPrefix}%`;
|
||||
const shippedRes = await runner.query(q.GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN, {
|
||||
companyId,
|
||||
pattern,
|
||||
excludeShipmentId: null,
|
||||
});
|
||||
const shippedAmountBySku = new Map();
|
||||
for (const row of shippedRes.rows ?? []) {
|
||||
const sku = String(row.sku ?? '').trim();
|
||||
shippedAmountBySku.set(sku, Number(row.shippedAmount || 0));
|
||||
}
|
||||
|
||||
// 6. Quantity ceiling check
|
||||
for (const [sku, proposedAmount] of proposedBySku.entries()) {
|
||||
const alreadyShipped = shippedAmountBySku.get(sku) || 0;
|
||||
const ordered = orderAmountBySku.get(sku) || 0;
|
||||
if (alreadyShipped + proposedAmount > ordered) {
|
||||
throw new Error(`Shipment amount exceeds ordered amount for article (${sku})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Resolve next index <y>
|
||||
const existingShipmentsRes = await runner.query(q.LIST_SHIPMENTS_BY_ORDER_NUMBER_PATTERN, {
|
||||
companyId,
|
||||
pattern,
|
||||
});
|
||||
const existingShipments = existingShipmentsRes.rows ?? [];
|
||||
|
||||
let maxY = 0;
|
||||
const prefixExact = `${order.number}${shipmentPrefix}`;
|
||||
for (const s of existingShipments) {
|
||||
const num = String(s.number || '');
|
||||
if (num.startsWith(prefixExact)) {
|
||||
const suffix = parseInt(num.slice(prefixExact.length), 10);
|
||||
if (Number.isInteger(suffix) && suffix > maxY) {
|
||||
maxY = suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextY = Math.max(existingShipments.length + 1, maxY + 1);
|
||||
const shipmentNumber = `${order.number}${shipmentPrefix}${nextY}`;
|
||||
|
||||
// 8. Create shipment manifest header
|
||||
const createdRes = await runner.query(q.CREATE_MANIFEST, {
|
||||
companyId,
|
||||
number: shipmentNumber,
|
||||
type: 'shipment',
|
||||
entityId: order.entityId ?? null,
|
||||
externalNumber,
|
||||
date: shipmentDate,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
const manifestId = createdRes.rows[0]?.id ?? createdRes.insertId;
|
||||
|
||||
// 9. Prepare lines copying defaults from order lines
|
||||
const linesToInsert = input.lines.map((line) => {
|
||||
const sku = String(line.sku).trim();
|
||||
const ol = orderLineBySku.get(sku);
|
||||
return {
|
||||
sku,
|
||||
name: line.name ?? ol?.name ?? sku,
|
||||
pu: line.pu !== undefined ? line.pu : (ol?.pu ?? 1),
|
||||
amount: Number(line.amount),
|
||||
net: line.net !== undefined ? Number(line.net) : Number(ol?.net ?? 0),
|
||||
rebate: line.rebate !== undefined ? Number(line.rebate) : Number(ol?.rebate ?? 0),
|
||||
taxrate: line.taxrate !== undefined ? Number(line.taxrate) : Number(ol?.taxrate ?? 0),
|
||||
comment: line.comment !== undefined ? (line.comment ? String(line.comment) : null) : (ol?.comment ?? null),
|
||||
};
|
||||
});
|
||||
|
||||
await insertLines(runner, {
|
||||
manifestId,
|
||||
lines: linesToInsert,
|
||||
ensureTax,
|
||||
});
|
||||
|
||||
await syncOrderSummary(runner, { companyId, manifestId: orderId });
|
||||
|
||||
return loadShipment(runner, q, companyId, manifestId);
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
return run(makeRunner(provider, tx), tx);
|
||||
}
|
||||
if (typeof provider?.transaction === 'function') {
|
||||
return provider.transaction((runner, innerTx) => run(runner, innerTx));
|
||||
}
|
||||
return run(makeRunner(provider));
|
||||
};
|
||||
}
|
||||
71
src/store/shipment/delete.js
Normal file
71
src/store/shipment/delete.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { queriesFor } from './helpers.js';
|
||||
import { syncOrderSummary } from '../order/syncSummary.js';
|
||||
|
||||
function requireId(val, name) {
|
||||
const n = Number(val);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a shipment manifest of the selected company.
|
||||
* Shipped shipments (with packages in 'shipped' state) cannot be deleted.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||
*/
|
||||
export default function del(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const q = queriesFor(provider);
|
||||
|
||||
/**
|
||||
* Deletes a shipment manifest.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The shipment manifest ID.
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If shipment not found or already has shipped packages.
|
||||
*
|
||||
* @example
|
||||
* await store.shipment.delete(googleAuthId, 42);
|
||||
*/
|
||||
return async (googleAuthId, id, tx) => {
|
||||
const shipmentId = requireId(id, 'Shipment');
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const check = await runner.query(q.GET_SHIPMENT_HEADER, {
|
||||
id: shipmentId,
|
||||
companyId,
|
||||
});
|
||||
if (!check.rows[0]) {
|
||||
throw new Error('Shipment not found');
|
||||
}
|
||||
|
||||
const shippedCheck = await runner.query(q.CHECK_SHIPMENT_PACKAGES_SHIPPED, {
|
||||
id: shipmentId,
|
||||
});
|
||||
if (shippedCheck.rows[0]) {
|
||||
throw new Error('Shipped shipment cannot be deleted');
|
||||
}
|
||||
|
||||
const shipmentNumber = String(check.rows[0].number || '');
|
||||
const prefixRes = await runner.query(q.GET_SHIPMENT_PREFIX, { companyId });
|
||||
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||
const pIdx = shipmentNumber.indexOf(shipmentPrefix);
|
||||
const orderNumber = pIdx > 0 ? shipmentNumber.slice(0, pIdx) : null;
|
||||
|
||||
await runner.query(q.DELETE_SHIPMENT, {
|
||||
id: shipmentId,
|
||||
companyId,
|
||||
});
|
||||
|
||||
if (orderNumber) {
|
||||
await syncOrderSummary(runner, { companyId, orderNumber });
|
||||
}
|
||||
};
|
||||
}
|
||||
46
src/store/shipment/getById.js
Normal file
46
src/store/shipment/getById.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { loadShipment, queriesFor } from './helpers.js';
|
||||
|
||||
function requireId(val, name) {
|
||||
const n = Number(val);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a shipment manifest by ID for the selected company, including lines and packages.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||
*/
|
||||
export default function getById(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const q = queriesFor(provider);
|
||||
|
||||
/**
|
||||
* Retrieves a shipment manifest by ID.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The shipment manifest ID.
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<object>} The shipment manifest.
|
||||
* @throws {Error} If shipment not found.
|
||||
*
|
||||
* @example
|
||||
* const shipment = await store.shipment.getById(googleAuthId, 42);
|
||||
*/
|
||||
return async (googleAuthId, id, tx) => {
|
||||
const shipmentId = requireId(id, 'Shipment');
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const shipment = await loadShipment(runner, q, companyId, shipmentId);
|
||||
if (!shipment) {
|
||||
throw new Error('Shipment not found');
|
||||
}
|
||||
|
||||
return shipment;
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user