diff --git a/API.md b/API.md index 359f8df..51397f6 100644 --- a/API.md +++ b/API.md @@ -66,6 +66,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt` - **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)` - **posterminalobituary**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt` +- **posterminalsyncqueue**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i` - **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f` - **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt` - **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)` @@ -272,6 +273,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `list(googleAuthId: number, tx?: object): Promise>` - `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>` - `recordObituary(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise` + - `recordSyncQueue(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise` - `setMaxKnownObituaryId(id: number, maxknownobituaryid: number|null, tx?: object): Promise<{id: number, maxknownobituaryid: number|null}>` - `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` diff --git a/API.posterminal.md b/API.posterminal.md index 54ef4b1..87b4d09 100644 --- a/API.posterminal.md +++ b/API.posterminal.md @@ -68,7 +68,7 @@ Finds a posterminal by its auth token. *Source: `src/store/posterminal/getCategoryCount.js`* -Counts categories assigned to the POS terminal updated after the given unix timestamp cursor. +Counts categories assigned to the POS terminal queued after the given sync queue cursor ID. **Parameters** @@ -76,7 +76,7 @@ Counts categories assigned to the POS terminal updated after the given unix time | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -87,7 +87,7 @@ Counts categories assigned to the POS terminal updated after the given unix time *Source: `src/store/posterminal/getCategoryList.js`* -Lists categories for a POS terminal modified after the cursor. +Lists categories for a POS terminal queued after the sync queue cursor ID. **Parameters** @@ -95,7 +95,7 @@ Lists categories for a POS terminal modified after the cursor. | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[input.limit=20]` | `number` | Max items to return. | | `[tx]` | `object` | Optional transaction. | @@ -107,7 +107,7 @@ Lists categories for a POS terminal modified after the cursor. *Source: `src/store/posterminal/getCompositeProductCount.js`* -Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor. +Counts composite products (Stücklisten) assigned to the POS terminal queued after the given sync queue cursor ID. **Parameters** @@ -115,7 +115,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -126,7 +126,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af *Source: `src/store/posterminal/getCustomerCount.js`* -Counts customers of the POS terminal company updated after the given unix timestamp cursor. +Counts customers of the POS terminal company queued after the given sync queue cursor ID. **Parameters** @@ -134,7 +134,7 @@ Counts customers of the POS terminal company updated after the given unix timest | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -145,7 +145,7 @@ Counts customers of the POS terminal company updated after the given unix timest *Source: `src/store/posterminal/getCustomerGroupCount.js`* -Counts customer groups of the POS terminal company updated after the given unix timestamp cursor. +Counts customer groups of the POS terminal company queued after the given sync queue cursor ID. **Parameters** @@ -153,7 +153,7 @@ Counts customer groups of the POS terminal company updated after the given unix | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -164,7 +164,7 @@ Counts customer groups of the POS terminal company updated after the given unix *Source: `src/store/posterminal/getCustomerGroupList.js`* -Lists customer groups for a POS terminal modified after the cursor, excluding 'Lieferanten'. +Lists customer groups for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'. **Parameters** @@ -172,7 +172,7 @@ Lists customer groups for a POS terminal modified after the cursor, excluding 'L | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[input.limit=50]` | `number` | Max items to return. | | `[tx]` | `object` | Optional transaction. | @@ -184,7 +184,7 @@ Lists customer groups for a POS terminal modified after the cursor, excluding 'L *Source: `src/store/posterminal/getCustomerList.js`* -Lists customers for a POS terminal modified after the cursor, excluding 'Lieferanten'. +Lists customers for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'. **Parameters** @@ -192,7 +192,7 @@ Lists customers for a POS terminal modified after the cursor, excluding 'Liefera | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[input.limit=20]` | `number` | Max items to return. | | `[tx]` | `object` | Optional transaction. | @@ -261,7 +261,7 @@ Returns the maximum order manifest id for the POS terminal company. *Source: `src/store/posterminal/getProductCount.js`* -Counts products assigned to the POS terminal updated after the given unix timestamp cursor. +Counts products assigned to the POS terminal queued after the given sync queue cursor ID. **Parameters** @@ -269,7 +269,7 @@ Counts products assigned to the POS terminal updated after the given unix timest | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -280,7 +280,7 @@ Counts products assigned to the POS terminal updated after the given unix timest *Source: `src/store/posterminal/getProductList.js`* -Lists products for a POS terminal modified after the cursor. +Lists products for a POS terminal queued after the sync queue cursor ID. **Parameters** @@ -288,7 +288,7 @@ Lists products for a POS terminal modified after the cursor. | --- | --- | --- | | `input` | `object` | | | `input.posterminalId` | `number` | The POS terminal id. | -| `[input.cursor=0]` | `number` | Unix timestamp cursor. | +| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. | | `[input.limit=20]` | `number` | Max items to return. | | `[tx]` | `object` | Optional transaction. | @@ -361,6 +361,26 @@ Records an obituary for all active paired POS terminals of a company. `Promise` +## `posterminal.recordSyncQueue` + +*Source: `src/store/posterminal/recordSyncQueue.js`* + +Enqueues a sync notification for all POS terminals of a company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.companyId` | `number` | The company id. | +| `input.entityType` | `string` | 'category' | 'article' | 'customer' | 'customergroup' | +| `input.entityId` | `number` | The entity's id. | +| `[tx]` | `object` | Optional transaction/runner. | + +**Returns** + +`Promise` + ## `posterminal.setMaxKnownObituaryId` *Source: `src/store/posterminal/setMaxKnownObituaryId.js`* diff --git a/generate-api-doc.js b/generate-api-doc.js index 56553a9..72cc374 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -382,6 +382,7 @@ const SCHEMA_TABLES = [ ['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'], ['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'], ['posterminalobituary', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt'], + ['posterminalsyncqueue', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i'], ['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'], ['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'], ['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], diff --git a/generate-schema-graph.js b/generate-schema-graph.js index b375c8f..aeb919e 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -170,8 +170,8 @@ function addInferredEdges(tables, edges) { const match = column.name.match(/^(.+)_id$/i); if (!match) continue; - // Obituary tables store references to deleted entities, not active foreign keys. - if (tableName.includes('obituary') || tableName.includes('tombstone')) continue; + // Obituary and sync queue tables store polymorphic entity_id references, not active foreign keys to entity. + if (tableName.includes('obituary') || tableName.includes('tombstone') || tableName.includes('syncqueue')) continue; const candidate = match[1]; if (!tables.has(candidate) || candidate === tableName) continue; @@ -390,7 +390,7 @@ const CLUSTERS = [ tables: ['tax', 'price', 'account', 'payment'], }, { id: 'units', label: 'Units', tables: ['unit'] }, - { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'posterminalobituary', 'printer', 'labeltemplate'] }, + { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'posterminalobituary', 'posterminalsyncqueue', 'printer', 'labeltemplate'] }, { id: 'ai', label: 'AI & Models', diff --git a/schema-graph.dot b/schema-graph.dot index d08e33c..b47b0bb 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -9,80 +9,80 @@ digraph schema { label="Identity & Company"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
sortINT
updated_atDATETIME2
>]; - meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; companyhistory [label=<
companyhistory
idINT PK
company_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - company [label=<
company
idINT PK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; + company [label=<
company
idINT PK
default_language_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
nameNVARCHAR(255)
>]; numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; - entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
sortINT
>]; - entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
number?NVARCHAR(100)
created_atDATETIME2
updated_atDATETIME2
>]; + meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
writeonlyBIT
>]; + entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
sortINT
>]; + customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
sortINT
updated_atDATETIME2
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
number?NVARCHAR(100)
created_atDATETIME2
updated_atDATETIME2
>]; entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
sortINT
>]; - companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; - entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; + entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; - categorytext [label=<
categorytext
idINT PK
category_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; - articletext [label=<
articletext
idINT PK
article_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; + articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; } subgraph cluster_catalog { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; - category [label=<
category
idINT PK
parent_id?INT FK
sortINT
updated_atDATETIME2
>]; - articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
meta?NVARCHAR(MAX)
>]; - article [label=<
article
idINT PK
company_idINT FK
parentattributename_id?INT FK
parentarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
track_stockBIT
is_parentBIT
parentsortINT
created_atDATETIME2
updated_atDATETIME2
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
tax_id?INT FK
article_idINT FK
net?FLOAT
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; - articlelist [label=<
articlelist
idINT PK
>]; - articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; - optionslist [label=<
optionslist
idINT PK
>]; - optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + articlelist [label=<
articlelist
idINT PK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; + optionslist [label=<
optionslist
idINT PK
>]; + optionslistarticle [label=<
optionslistarticle
idINT PK
optionslist_idINT FK
article_idINT FK
countINT
sortINT
mandatedBIT
>]; + articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
meta?NVARCHAR(MAX)
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + article [label=<
article
idINT PK
unit_idINT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
parentarticle_id?INT FK
parentattributename_id?INT FK
company_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
track_stockBIT
is_parentBIT
parentsortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlesupplier [label=<
articlesupplier
idINT PK
tax_id?INT FK
article_idINT FK
entity_idINT FK
net?FLOAT
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; + category [label=<
category
idINT PK
parent_id?INT FK
sortINT
updated_atDATETIME2
>]; + categoryarticle [label=<
categoryarticle
idINT PK
article_idINT FK
category_idINT FK
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
smallthumbwINT
smallthumbhINT
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
smallthumbbinaryVARBINARY(MAX)
>]; - attributevaluepic [label=<
attributevaluepic
idINT PK
attributevalue_idINT FK
pic_idINT FK
sortINT
>]; - categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; - articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; - brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; + pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
smallthumbwINT
smallthumbhINT
smallthumbbinaryVARBINARY(MAX)
sourceNVARCHAR(MAX)
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; + attributevaluepic [label=<
attributevaluepic
idINT PK
pic_idINT FK
attributevalue_idINT FK
sortINT
>]; file [label=<
file
idINT PK
hashNVARCHAR(64)
mimetypeNVARCHAR(255)
nameNVARCHAR(255)
sizeBIGINT
createdDATETIME2
modifiedDATETIME2
contentVARBINARY(MAX)
thumbavif?VARBINARY(MAX)
thumbx?INT
thumby?INT
meta?NVARCHAR(MAX)
>]; } subgraph cluster_manifests { label="Documents & Shipping"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - deliverysummary [label=<
deliverysummary
idINT PK
company_idINT FK
supplier_id?INT FK
manifest_idINT FK
delivery_numberNVARCHAR(100)
external_number?NVARCHAR(100)
delivery_dateDATETIME2
created_atDATETIME2
supplier_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
net_totalFLOAT
total_quantityFLOAT
stock_positionsINT
booked_positionsINT
unbooked_positionsINT
stock_quantityFLOAT
booked_quantityFLOAT
unbooked_quantityFLOAT
is_bookedTINYINT
>]; - ordersummary [label=<
ordersummary
idINT PK
company_idINT FK
entity_id?INT FK
manifest_idINT FK
order_numberNVARCHAR(100)
order_dateDATETIME2
created_atDATETIME2
customer_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
paid_totalFLOAT
ordered_quantityFLOAT
shipped_quantityFLOAT
is_paidTINYINT
is_shippedTINYINT
>]; - quotesummary [label=<
quotesummary
idINT PK
company_idINT FK
entity_id?INT FK
manifest_idINT FK
quote_numberNVARCHAR(100)
quote_dateDATETIME2
created_atDATETIME2
customer_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
total_quantityFLOAT
>]; - shipmentsummary [label=<
shipmentsummary
idINT PK
company_idINT FK
entity_id?INT FK
manifest_idINT FK
shipment_numberNVARCHAR(100)
shipment_dateDATETIME2
created_atDATETIME2
order_id?INT
order_number?NVARCHAR(100)
customer_name?NVARCHAR(255)
positionsINT
total_quantityFLOAT
package_countINT
carrier?NVARCHAR(100)
tracking_code?NVARCHAR(255)
stateNVARCHAR(50)
>]; - package [label=<
package
idINT PK
entityaddress_idINT FK
shipping_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
stateNVARCHAR(100)
details?NVARCHAR(MAX)
>]; + articlewarehousesummary [label=<
articlewarehousesummary
idINT PK
article_idINT FK
warehouse_idINT FK
stockFLOAT
>]; manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateFLOAT
shippableTINYINT
>]; - manifest [label=<
manifest
idINT PK
company_idINT FK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; - manifestattachment [label=<
manifestattachment
idINT PK
manifest_idINT FK
file_idINT FK
sortINT
>]; - shipping [label=<
shipping
idINT PK
carrier_idINT FK
manifest_idINT FK
numberNVARCHAR(100)
tracking_code?NVARCHAR(255)
typeNVARCHAR(100)
stateNVARCHAR(100)
>]; - warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
>]; + manifest [label=<
manifest
idINT PK
company_idINT FK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; + manifestattachment [label=<
manifestattachment
idINT PK
file_idINT FK
manifest_idINT FK
sortINT
>]; + shipping [label=<
shipping
idINT PK
manifest_idINT FK
carrier_idINT FK
numberNVARCHAR(100)
tracking_code?NVARCHAR(255)
typeNVARCHAR(100)
stateNVARCHAR(100)
>]; ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
warehouse_id?INT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
created_atDATETIME2
>]; - articlewarehousesummary [label=<
articlewarehousesummary
idINT PK
warehouse_idINT FK
article_idINT FK
stockFLOAT
>]; + warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
>]; + deliverysummary [label=<
deliverysummary
idINT PK
manifest_idINT FK
company_idINT FK
supplier_id?INT FK
delivery_numberNVARCHAR(100)
external_number?NVARCHAR(100)
delivery_dateDATETIME2
created_atDATETIME2
supplier_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
net_totalFLOAT
total_quantityFLOAT
stock_positionsINT
booked_positionsINT
unbooked_positionsINT
stock_quantityFLOAT
booked_quantityFLOAT
unbooked_quantityFLOAT
is_bookedTINYINT
>]; + package [label=<
package
idINT PK
shipping_idINT FK
entityaddress_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
stateNVARCHAR(100)
details?NVARCHAR(MAX)
>]; + ordersummary [label=<
ordersummary
idINT PK
manifest_idINT FK
company_idINT FK
entity_id?INT FK
order_numberNVARCHAR(100)
order_dateDATETIME2
created_atDATETIME2
customer_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
paid_totalFLOAT
ordered_quantityFLOAT
shipped_quantityFLOAT
is_paidTINYINT
is_shippedTINYINT
>]; + quotesummary [label=<
quotesummary
idINT PK
manifest_idINT FK
company_idINT FK
entity_id?INT FK
quote_numberNVARCHAR(100)
quote_dateDATETIME2
created_atDATETIME2
customer_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
total_quantityFLOAT
>]; + shipmentsummary [label=<
shipmentsummary
idINT PK
manifest_idINT FK
company_idINT FK
entity_id?INT FK
shipment_numberNVARCHAR(100)
shipment_dateDATETIME2
created_atDATETIME2
order_id?INT
order_number?NVARCHAR(100)
customer_name?NVARCHAR(255)
positionsINT
total_quantityFLOAT
package_countINT
carrier?NVARCHAR(100)
tracking_code?NVARCHAR(255)
stateNVARCHAR(50)
>]; } subgraph cluster_commerce { label="Pricing & Payments"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - payment [label=<
payment
idINT PK
manifest_idINT FK
account_idINT FK
typeNVARCHAR(100)
amountFLOAT
>]; account [label=<
account
idINT PK
company_idINT FK
typeNVARCHAR(100)
nameNVARCHAR(255)
>]; - price [label=<
price
idINT PK
customergroup_idINT FK
tax_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + price [label=<
price
idINT PK
tax_idINT FK
article_idINT FK
customergroup_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + payment [label=<
payment
idINT PK
account_idINT FK
manifest_idINT FK
typeNVARCHAR(100)
amountFLOAT
created_atDATETIME2
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -94,152 +94,162 @@ digraph schema { label="POS"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - printer [label=<
printer
idINT PK
company_idINT FK
addressNVARCHAR(255)
typeNVARCHAR(50)
model?NVARCHAR(255)
serial?NVARCHAR(255)
sortINT
desc?NVARCHAR(MAX)
>]; - labeltemplate [label=<
labeltemplate
idINT PK
company_idINT FK
nameNVARCHAR(255)
widthINT
heightINT
dpiINT
typeNVARCHAR(50)
contentNVARCHAR(MAX)
thumb?VARBINARY(MAX)
>]; posterminalobituary [label=<
posterminalobituary
idINT PK
posterminal_idINT FK
entity_typeNVARCHAR(50)
entity_idINT
created_atDATETIME2
>]; - posterminal [label=<
posterminal
idINT PK
company_idINT FK
root_category_id?INT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpin?NVARCHAR(50)
paired_at?DATETIME2
clientserial?NVARCHAR(255)
maxknownobituaryid?INT
sync_root_articlesBIT
lastseen_at?DATETIME2
updated_atDATETIME2
>]; - posterminalcategory [label=<
posterminalcategory
idINT PK
posterminal_idINT FK
category_idINT FK
>]; + posterminal [label=<
posterminal
idINT PK
root_category_id?INT FK
company_idINT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpin?NVARCHAR(50)
paired_at?DATETIME2
clientserial?NVARCHAR(255)
maxknownobituaryid?INT
sync_root_articlesBIT
lastseen_at?DATETIME2
updated_atDATETIME2
>]; + posterminalsyncqueue [label=<
posterminalsyncqueue
idINT PK
posterminal_idINT FK
entity_typeNVARCHAR(50)
entity_idINT
>]; + posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; + labeltemplate [label=<
labeltemplate
idINT PK
company_idINT FK
nameNVARCHAR(255)
widthINT
heightINT
dpiINT
typeNVARCHAR(50)
contentNVARCHAR(MAX)
thumb?VARBINARY(MAX)
>]; + printer [label=<
printer
idINT PK
company_idINT FK
addressNVARCHAR(255)
typeNVARCHAR(50)
model?NVARCHAR(255)
serial?NVARCHAR(255)
sortINT
desc?NVARCHAR(MAX)
>]; } subgraph cluster_ai { label="AI & Models"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - modelvfs [label=<
modelvfs
idINT PK
company_idINT FK
nameNVARCHAR(255)
mimeNVARCHAR(100)
contentVARBINARY(MAX)
metaNVARCHAR(MAX)
created_atDATETIME2
updated_atDATETIME2
>]; subagentschedule [label=<
subagentschedule
idINT PK
company_idINT FK
desc?NVARCHAR(MAX)
promptNVARCHAR(MAX)
skills?NVARCHAR(MAX)
lastagentnote?NVARCHAR(MAX)
max_itrINT
cronNVARCHAR(100)
created_atDATETIME2
lastrun_at?DATETIME2
>]; modelconversation [label=<
modelconversation
idINT PK
companyuser_idINT FK
providerNVARCHAR(100)
modelNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
stateNVARCHAR(20)
started_by?INT
>]; modelmessage [label=<
modelmessage
idINT PK
conversation_idINT FK
created_atDATETIME2
roleNVARCHAR(20)
messageNVARCHAR(MAX)
>]; + modelvfs [label=<
modelvfs
idINT PK
company_idINT FK
nameNVARCHAR(255)
mimeNVARCHAR(100)
contentVARBINARY(MAX)
metaNVARCHAR(MAX)
created_atDATETIME2
updated_atDATETIME2
>]; } subgraph cluster_attributes { label="Properties & Attributes"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - attributevalue [label=<
attributevalue
idINT PK
attribute_idINT FK
valueNVARCHAR(255)
sortINT
>]; - attributename [label=<
attributename
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; + articleproperty [label=<
articleproperty
idINT PK
article_idINT FK
property_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; + articleattributevalue [label=<
articleattributevalue
idINT PK
article_idINT FK
attributevalue_idINT FK
sortINT
>]; + categoryproperty [label=<
categoryproperty
idINT PK
category_idINT FK
property_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; + categoryattributevalue [label=<
categoryattributevalue
idINT PK
category_idINT FK
attributevalue_idINT FK
sortINT
>]; propertyname [label=<
propertyname
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; - categoryproperty [label=<
categoryproperty
idINT PK
property_idINT FK
category_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; - categoryattributevalue [label=<
categoryattributevalue
idINT PK
attributevalue_idINT FK
category_idINT FK
sortINT
>]; - articleproperty [label=<
articleproperty
idINT PK
property_idINT FK
article_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; - articleattributevalue [label=<
articleattributevalue
idINT PK
attributevalue_idINT FK
article_idINT FK
sortINT
>]; + attributename [label=<
attributename
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; + attributevalue [label=<
attributevalue
idINT PK
attribute_idINT FK
valueNVARCHAR(255)
sortINT
>]; } + returnsummary [label=<
returnsummary
idINT PK
manifest_idINT FK
company_idINT FK
supplier_id?INT FK
return_numberNVARCHAR(100)
external_number?NVARCHAR(100)
return_dateDATETIME2
created_atDATETIME2
supplier_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
net_totalFLOAT
total_quantityFLOAT
stock_positionsINT
booked_positionsINT
unbooked_positionsINT
stock_quantityFLOAT
booked_quantityFLOAT
unbooked_quantityFLOAT
is_bookedTINYINT
>]; + retouresummary [label=<
retouresummary
idINT PK
manifest_idINT FK
company_idINT FK
entity_id?INT FK
retoure_numberNVARCHAR(100)
retoure_dateDATETIME2
created_atDATETIME2
customer_name?NVARCHAR(255)
positionsINT
gross_totalFLOAT
paid_totalFLOAT
ordered_quantityFLOAT
shipped_quantityFLOAT
is_paidTINYINT
is_shippedTINYINT
>]; carrier [label=<
carrier
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
has_packagesBIT
is_reverseBIT
>]; - mcp_auth_code [label=<
mcp_auth_code
codeNVARCHAR(128) PK
google_auth_idINT FK
client_idNVARCHAR(100)
redirect_uriNVARCHAR(2048)
code_challengeNVARCHAR(255)
code_challenge_methodNVARCHAR(32)
scope?NVARCHAR(255)
expires_atDATETIME2
usedTINYINT
created_atDATETIME2
>]; - mcp_token [label=<
mcp_token
idINT PK
google_auth_idINT FK
refresh_tokenNVARCHAR(128)
client_idNVARCHAR(100)
scope?NVARCHAR(255)
expires_atDATETIME2
revokedTINYINT
created_atDATETIME2
>]; mcp_client [label=<
mcp_client
idINT PK
client_idNVARCHAR(100)
client_secret?NVARCHAR(255)
client_nameNVARCHAR(255)
redirect_urisNVARCHAR(MAX)
grant_types?NVARCHAR(255)
response_types?NVARCHAR(255)
scope?NVARCHAR(255)
created_atDATETIME2
>]; healthcheck [label=<
healthcheck
idINT PK
sequenceBIGINT
updated_atDATETIME2
>]; + mcp_auth_code [label=<
mcp_auth_code
codeNVARCHAR(128) PK
google_auth_idINT FK
client_idNVARCHAR(100)
redirect_uriNVARCHAR(2048)
code_challengeNVARCHAR(255)
code_challenge_methodNVARCHAR(32)
scope?NVARCHAR(255)
expires_atDATETIME2
usedTINYINT
created_atDATETIME2
>]; + mcp_token [label=<
mcp_token
idINT PK
google_auth_idINT FK
refresh_tokenNVARCHAR(128)
client_idNVARCHAR(100)
scope?NVARCHAR(255)
expires_atDATETIME2
revokedTINYINT
created_atDATETIME2
>]; __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:w -> company:in11:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; + google_auth:selected_company:w -> company:in19:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; category:parent_id:e -> category:id:e [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_category_parent", constraint=false]; - company:root_category_id:w -> category:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; - company:default_language_id:w -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; - company:taxrate:w -> tax:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; - company:reducedtaxrate:w -> tax:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; - company:taxfreerate:w -> tax:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; - customergroup:company_id:nw -> company:in6:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; - company:poscustomergroup_id:nw -> customergroup:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; - entity:customergroup_id:w -> customergroup:in2:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; - entityaddress:entity_id:sw -> entity:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; - entitycontact:entity_id:se -> entity:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; - entityhistory:entity_id:w -> entity:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; - entityhistory:companyuser_id:se -> companyuser:in3:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; - unit:company_id:e -> company:in23:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; - unittext:unit_id:se -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; - unittext:language_id:e -> language:in3:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; - brand:company_id:e -> company:in22:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; - article:company_id:w -> company:in21:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; - article:unit_id:e -> unit:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; - article:brand_id:w -> brand:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; + company:root_category_id:w -> category:in6:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; + company:default_language_id:w -> language:in3:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; + company:taxrate:w -> tax:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; + company:reducedtaxrate:w -> tax:in2:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; + company:taxfreerate:w -> tax:in3:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; + customergroup:company_id:w -> company:in18:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; + company:poscustomergroup_id:e -> customergroup:in1:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; + entity:customergroup_id:se -> customergroup:in2:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; + entityaddress:entity_id:ne -> entity:in9:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; + entitycontact:entity_id:sw -> entity:in8:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; + entityhistory:entity_id:sw -> entity:in10:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; + entityhistory:companyuser_id:sw -> companyuser:in2:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; + unit:company_id:e -> company:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; + unittext:unit_id:sw -> unit:in0:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; + unittext:language_id:w -> language:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; + brand:company_id:w -> company:in1:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; + article:company_id:w -> company:in3:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; + article:unit_id:e -> unit:in1:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; + article:brand_id:w -> brand:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; article:articlelist_id:w -> articlelist:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_articlelist", constraint=false, weight=1]; - article:optionslist_id:w -> optionslist:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; + article:optionslist_id:w -> optionslist:in1:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; article:parentarticle_id:e -> article:id:e [color="#c79bff", fontcolor="#c79bff", tooltip="FK_article_parentarticle", constraint=false]; - article:parentattributename_id:se -> attributename:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_parentattributename", constraint=false, weight=1]; - article:parentattributename_id:se -> attributename:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_parentattributename", constraint=false, weight=1]; - articlehistory:article_id:sw -> article:in3:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_article", weight=3]; - articlehistory:companyuser_id:e -> companyuser:in2:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; - companyhistory:company_id:sw -> company:in7:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_companyhistory_company", weight=3]; - companyhistory:companyuser_id:e -> companyuser:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_companyhistory_companyuser", constraint=false, weight=1]; - posterminalobituary:posterminal_id:se -> posterminal:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_posterminalobituary_posterminal", weight=3]; - articleean:article_id:se -> article:in7:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articleean_article", weight=3]; - articlepic:article_id:sw -> article:in11:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlepic_article", weight=3]; - articlepic:pic_id:e -> pic:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlepic_pic", constraint=false, weight=1]; - brandpic:brand_id:sw -> brand:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_brandpic_brand", weight=3]; - brandpic:pic_id:w -> pic:in3:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_brandpic_pic", constraint=false, weight=1]; - attributevaluepic:attributevalue_id:e -> attributevalue:in2:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_attributevaluepic_attributevalue", weight=3]; - attributevaluepic:pic_id:w -> pic:in0:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_attributevaluepic_pic", constraint=false, weight=1]; - articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_articlelistarticle_articlelist", weight=3]; - articlelistarticle:article_id:w -> article:in9:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_optionslistarticle_optionslist", weight=3]; - optionslistarticle:article_id:e -> article:in10:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; - categoryarticle:category_id:se -> category:in5:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_categoryarticle_category", weight=3]; - categoryarticle:article_id:sw -> article:in4:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; - categorypic:category_id:sw -> category:in7:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_categorypic_category", weight=3]; - categorypic:pic_id:e -> pic:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_categorypic_pic", constraint=false, weight=1]; - categorytext:category_id:w -> category:in8:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_categorytext_category", weight=3]; - categorytext:language_id:e -> language:in1:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_categorytext_language", constraint=false, weight=1]; - articletext:article_id:e -> article:in12:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articletext_article", weight=3]; - articletext:language_id:w -> language:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articletext_language", constraint=false, weight=1]; - price:tax_id:w -> tax:in3:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_price_tax", constraint=false, weight=1]; - price:article_id:nw -> article:in5:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_price_article", weight=3]; - price:customergroup_id:e -> customergroup:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:se -> article:in8:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_article", weight=3]; - articlesupplier:entity_id:e -> entity:in8:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; - articlesupplier:tax_id:e -> tax:in4:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlesupplier_tax", constraint=false, weight=1]; - manifest:company_id:w -> company:in18:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_manifest_company", weight=3]; - manifest:entity_id:e -> entity:in7:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_manifest_entity", constraint=false, weight=1]; - manifestarticle:manifest_id:sw -> manifest:in4:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_manifestarticle_manifest", weight=3]; - warehouse:company_id:e -> company:in19:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_warehouse_company", weight=3]; - ledger:manifestarticle_id:w -> manifestarticle:in0:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_ledger_manifestarticle", constraint=false, weight=1]; - ledger:warehouse_id:sw -> warehouse:in0:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_ledger_warehouse", weight=3]; - numbers:company_id:se -> company:in8:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_numbers_company", weight=3]; - companyuser:google_auth_id:w -> google_auth:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; - companyuser:company_id:e -> company:in10:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_companyuser_company", weight=3]; - modelconversation:companyuser_id:w -> companyuser:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_modelconversation_companyuser", weight=3]; - modelvfs:company_id:sw -> company:in3:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_modelvfs_company", weight=3]; - modelmessage:conversation_id:se -> modelconversation:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_modelmessage_conversation", weight=3]; - companysetting:company_id:se -> company:in9:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_companysetting_company", weight=3]; - posterminal:company_id:e -> company:in4:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_posterminal_company", weight=3]; - posterminal:root_category_id:w -> category:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_posterminal_root_category", constraint=false, weight=1]; - posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_posterminalcategory_posterminal", weight=3]; - posterminalcategory:category_id:w -> category:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; - printer:company_id:w -> company:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_printer_company", weight=3]; - labeltemplate:company_id:e -> company:in1:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_labeltemplate_company", weight=3]; - manifestattachment:manifest_id:ne -> manifest:in5:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifestattachment_manifest", weight=3]; - manifestattachment:file_id:w -> file:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; - propertyname:company_id:e -> company:in16:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_propertyname_company", weight=3]; - articleproperty:article_id:w -> article:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articleproperty_article", weight=3]; - articleproperty:property_id:e -> propertyname:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articleproperty_property", constraint=false, weight=1]; - attributename:company_id:e -> company:in17:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_attributename_company", weight=3]; - attributevalue:attribute_id:se -> attributename:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_attributevalue_attribute", weight=3]; - articleattributevalue:article_id:e -> article:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_articleattributevalue_article", weight=3]; - articleattributevalue:attributevalue_id:e -> attributevalue:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_articleattributevalue_attributevalue", constraint=false, weight=1]; - categoryproperty:category_id:w -> category:in3:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_categoryproperty_category", weight=3]; - categoryproperty:property_id:e -> propertyname:in0:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categoryproperty_property", constraint=false, weight=1]; - categoryattributevalue:category_id:w -> category:in4:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categoryattributevalue_category", weight=3]; - categoryattributevalue:attributevalue_id:e -> attributevalue:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categoryattributevalue_attributevalue", constraint=false, weight=1]; - account:company_id:w -> company:in20:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_account_company", weight=3]; - payment:account_id:w -> account:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_payment_account", constraint=false, weight=1]; - payment:manifest_id:e -> manifest:in7:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_payment_manifest", weight=3]; - carrier:company_id:w -> company:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_carrier_company", weight=3]; - shipping:manifest_id:sw -> manifest:in6:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_shipping_manifest", weight=3]; - shipping:carrier_id:e -> carrier:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_shipping_carrier", constraint=false, weight=1]; - package:shipping_id:e -> shipping:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_package_shipping", constraint=false, weight=1]; - package:entityaddress_id:e -> entityaddress:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_package_entityaddress", weight=3]; - ordersummary:company_id:se -> company:in13:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_ordersummary_company", weight=3]; - ordersummary:manifest_id:w -> manifest:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_ordersummary_manifest", constraint=false, weight=1]; - ordersummary:entity_id:e -> entity:in4:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_ordersummary_entity", constraint=false, weight=1]; - quotesummary:company_id:se -> company:in14:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_quotesummary_company", weight=3]; - quotesummary:manifest_id:w -> manifest:in2:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_quotesummary_manifest", constraint=false, weight=1]; - quotesummary:entity_id:e -> entity:in5:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_quotesummary_entity", constraint=false, weight=1]; - shipmentsummary:company_id:se -> company:in15:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_shipmentsummary_company", weight=3]; - shipmentsummary:manifest_id:w -> manifest:in3:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_shipmentsummary_manifest", constraint=false, weight=1]; - shipmentsummary:entity_id:e -> entity:in6:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_shipmentsummary_entity", constraint=false, weight=1]; - deliverysummary:company_id:se -> company:in12:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_deliverysummary_company", weight=3]; - deliverysummary:manifest_id:w -> manifest:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_deliverysummary_manifest", constraint=false, weight=1]; - deliverysummary:supplier_id:se -> entity:in3:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_deliverysummary_supplier", constraint=false, weight=1]; - articlewarehousesummary:article_id:sw -> article:in2:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_articlewarehousesummary_article", weight=3]; - articlewarehousesummary:warehouse_id:se -> warehouse:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_articlewarehousesummary_warehouse", constraint=false, weight=1]; - mcp_auth_code:google_auth_id:se -> google_auth:in1:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_mcp_auth_code_google_auth", weight=3]; - mcp_token:google_auth_id:w -> google_auth:in2:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_mcp_token_google_auth", weight=3]; - subagentschedule:company_id:sw -> company:in5:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_subagentschedule_company", weight=3]; + article:parentattributename_id:se -> attributename:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_parentattributename", constraint=false, weight=1]; + article:parentattributename_id:se -> attributename:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_parentattributename", constraint=false, weight=1]; + articlehistory:article_id:se -> article:in4:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_article", weight=3]; + articlehistory:companyuser_id:e -> companyuser:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; + companyhistory:company_id:ne -> company:in15:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_companyhistory_company", weight=3]; + companyhistory:companyuser_id:w -> companyuser:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_companyhistory_companyuser", constraint=false, weight=1]; + posterminalobituary:posterminal_id:ne -> posterminal:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_posterminalobituary_posterminal", weight=3]; + posterminalsyncqueue:posterminal_id:sw -> posterminal:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_posterminalsyncqueue_posterminal", weight=3]; + articleean:article_id:se -> article:in6:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articleean_article", weight=3]; + articlepic:article_id:se -> article:in5:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlepic_article", weight=3]; + articlepic:pic_id:w -> pic:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_articlepic_pic", constraint=false, weight=1]; + brandpic:brand_id:e -> brand:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_brandpic_brand", weight=3]; + brandpic:pic_id:w -> pic:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_brandpic_pic", constraint=false, weight=1]; + attributevaluepic:attributevalue_id:w -> attributevalue:in0:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_attributevaluepic_attributevalue", weight=3]; + attributevaluepic:pic_id:w -> pic:in3:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_attributevaluepic_pic", constraint=false, weight=1]; + articlelistarticle:articlelist_id:sw -> articlelist:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_articlelistarticle_articlelist", weight=3]; + articlelistarticle:article_id:e -> article:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + optionslistarticle:optionslist_id:sw -> optionslist:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_optionslistarticle_optionslist", weight=3]; + optionslistarticle:article_id:e -> article:in3:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + categoryarticle:category_id:nw -> category:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_categoryarticle_category", weight=3]; + categoryarticle:article_id:sw -> article:in10:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; + categorypic:category_id:sw -> category:in3:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_categorypic_category", weight=3]; + categorypic:pic_id:w -> pic:in2:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_categorypic_pic", constraint=false, weight=1]; + categorytext:category_id:e -> category:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_categorytext_category", weight=3]; + categorytext:language_id:e -> language:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_categorytext_language", constraint=false, weight=1]; + articletext:article_id:e -> article:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articletext_article", weight=3]; + articletext:language_id:w -> language:in1:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articletext_language", constraint=false, weight=1]; + price:tax_id:w -> tax:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_price_tax", constraint=false, weight=1]; + price:article_id:sw -> article:in2:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_price_article", weight=3]; + price:customergroup_id:e -> customergroup:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_price_customergroup", constraint=false, weight=1]; + articlesupplier:article_id:sw -> article:in8:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articlesupplier_article", weight=3]; + articlesupplier:entity_id:e -> entity:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; + articlesupplier:tax_id:w -> tax:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlesupplier_tax", constraint=false, weight=1]; + manifest:company_id:e -> company:in4:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_manifest_company", weight=3]; + manifest:entity_id:e -> entity:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_manifest_entity", constraint=false, weight=1]; + manifestarticle:manifest_id:se -> manifest:in2:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_manifestarticle_manifest", weight=3]; + warehouse:company_id:se -> company:in7:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_warehouse_company", weight=3]; + ledger:manifestarticle_id:w -> manifestarticle:in0:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_ledger_manifestarticle", constraint=false, weight=1]; + ledger:warehouse_id:sw -> warehouse:in1:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_ledger_warehouse", weight=3]; + numbers:company_id:sw -> company:in16:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_numbers_company", weight=3]; + companyuser:google_auth_id:e -> google_auth:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; + companyuser:company_id:w -> company:in20:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_companyuser_company", weight=3]; + modelconversation:companyuser_id:w -> companyuser:in3:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_modelconversation_companyuser", weight=3]; + modelvfs:company_id:w -> company:in22:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_modelvfs_company", weight=3]; + modelmessage:conversation_id:sw -> modelconversation:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_modelmessage_conversation", weight=3]; + companysetting:company_id:sw -> company:in17:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_companysetting_company", weight=3]; + posterminal:company_id:w -> company:in23:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_posterminal_company", weight=3]; + posterminal:root_category_id:w -> category:in7:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_posterminal_root_category", constraint=false, weight=1]; + posterminalcategory:posterminal_id:sw -> posterminal:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_posterminalcategory_posterminal", weight=3]; + posterminalcategory:category_id:e -> category:in8:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; + printer:company_id:w -> company:in24:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_printer_company", weight=3]; + labeltemplate:company_id:w -> company:in25:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_labeltemplate_company", weight=3]; + manifestattachment:manifest_id:nw -> manifest:in3:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_manifestattachment_manifest", weight=3]; + manifestattachment:file_id:e -> file:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; + propertyname:company_id:e -> company:in5:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_propertyname_company", weight=3]; + articleproperty:article_id:w -> article:in9:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articleproperty_article", weight=3]; + articleproperty:property_id:e -> propertyname:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articleproperty_property", constraint=false, weight=1]; + attributename:company_id:w -> company:in6:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_attributename_company", weight=3]; + attributevalue:attribute_id:se -> attributename:in2:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_attributevalue_attribute", weight=3]; + articleattributevalue:article_id:e -> article:in12:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_articleattributevalue_article", weight=3]; + articleattributevalue:attributevalue_id:w -> attributevalue:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_articleattributevalue_attributevalue", constraint=false, weight=1]; + categoryproperty:category_id:w -> category:in5:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categoryproperty_category", weight=3]; + categoryproperty:property_id:e -> propertyname:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categoryproperty_property", constraint=false, weight=1]; + categoryattributevalue:category_id:w -> category:in4:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categoryattributevalue_category", weight=3]; + categoryattributevalue:attributevalue_id:e -> attributevalue:in2:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categoryattributevalue_attributevalue", constraint=false, weight=1]; + account:company_id:w -> company:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_account_company", weight=3]; + payment:account_id:w -> account:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_payment_account", constraint=false, weight=1]; + payment:manifest_id:e -> manifest:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_payment_manifest", weight=3]; + carrier:company_id:e -> company:in11:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_carrier_company", weight=3]; + shipping:manifest_id:nw -> manifest:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_shipping_manifest", weight=3]; + shipping:carrier_id:se -> carrier:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_shipping_carrier", constraint=false, weight=1]; + package:shipping_id:w -> shipping:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_package_shipping", constraint=false, weight=1]; + package:entityaddress_id:e -> entityaddress:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_package_entityaddress", weight=3]; + ordersummary:company_id:sw -> company:in13:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_ordersummary_company", weight=3]; + ordersummary:manifest_id:e -> manifest:in8:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_ordersummary_manifest", constraint=false, weight=1]; + ordersummary:entity_id:w -> entity:in5:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_ordersummary_entity", constraint=false, weight=1]; + quotesummary:company_id:se -> company:in12:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_quotesummary_company", weight=3]; + quotesummary:manifest_id:e -> manifest:in6:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_quotesummary_manifest", constraint=false, weight=1]; + quotesummary:entity_id:e -> entity:in6:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_quotesummary_entity", constraint=false, weight=1]; + shipmentsummary:company_id:ne -> company:in14:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_shipmentsummary_company", weight=3]; + shipmentsummary:manifest_id:w -> manifest:in9:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_shipmentsummary_manifest", constraint=false, weight=1]; + shipmentsummary:entity_id:e -> entity:in7:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_shipmentsummary_entity", constraint=false, weight=1]; + deliverysummary:company_id:nw -> company:in8:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_deliverysummary_company", weight=3]; + deliverysummary:manifest_id:e -> manifest:in4:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_deliverysummary_manifest", constraint=false, weight=1]; + deliverysummary:supplier_id:w -> entity:in2:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_deliverysummary_supplier", constraint=false, weight=1]; + returnsummary:company_id:e -> company:in10:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_returnsummary_company", weight=3]; + returnsummary:manifest_id:w -> manifest:in5:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_returnsummary_manifest", constraint=false, weight=1]; + returnsummary:supplier_id:w -> entity:in3:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_returnsummary_supplier", constraint=false, weight=1]; + retouresummary:company_id:w -> company:in9:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_retouresummary_company", weight=3]; + retouresummary:manifest_id:e -> manifest:in7:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_retouresummary_manifest", constraint=false, weight=1]; + retouresummary:entity_id:e -> entity:in4:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_retouresummary_entity", constraint=false, weight=1]; + articlewarehousesummary:article_id:nw -> article:in11:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_articlewarehousesummary_article", weight=3]; + articlewarehousesummary:warehouse_id:e -> warehouse:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_articlewarehousesummary_warehouse", constraint=false, weight=1]; + mcp_auth_code:google_auth_id:sw -> google_auth:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_mcp_auth_code_google_auth", weight=3]; + mcp_token:google_auth_id:w -> google_auth:in2:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_mcp_token_google_auth", weight=3]; + subagentschedule:company_id:w -> company:in21:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_subagentschedule_company", weight=3]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 36260be..8c8a8ee 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,377 +4,355 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_manifests - -Documents & Shipping + +Documents & Shipping cluster_commerce - -Pricing & Payments + +Pricing & Payments cluster_units - -Units + +Units cluster_pos - -POS + +POS cluster_ai - -AI & Models + +AI & Models cluster_attributes - -Properties & Attributes - - - -customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -updated_at -DATETIME2 - - - -company - - - - - - - - - - - - - - - - - - - - - - - - - - -company - - -id -INT -PK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -root_category_id -? -INT -FK - - -default_language_id -? -INT -FK - - -name -NVARCHAR(255) - - - -customergroup:nw->company:n - - - - - - - - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + +Properties & Attributes - + companyhistory - - -companyhistory - - -id -INT -PK - - -company_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +companyhistory + + +id +INT +PK + + +company_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +company + + + + + + + + + + + + + + + + + + + + + + + + + + + + +company + + +id +INT +PK + + +default_language_id +? +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +root_category_id +? +INT +FK + + +poscustomergroup_id +INT +FK + + +name +NVARCHAR(255) -companyhistory:sw->company:n +companyhistory:ne->company:n - - + + - + companyuser - - - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK + + + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK -companyhistory:e->companyuser:n +companyhistory:w->companyuser:n - - + + + + +customergroup + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +updated_at +DATETIME2 + -company:nw->customergroup:n +company:e->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) company:w->language:n - - + + - + category - - - - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT - - -updated_at -DATETIME2 + + + + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT + + +updated_at +DATETIME2 company:w->category:n - - + + tax - - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) company:w->tax:n - - + + @@ -382,8 +360,8 @@ company:w->tax:n - - + + @@ -391,755 +369,926 @@ company:w->tax:n - - + + - + numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - -numbers:se->company:n - - - + +numbers:sw->company:n + + + + + + + + +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + +writeonly +BIT + + + +companysetting:sw->company:n + + + entityaddress - - - -entityaddress - - -id -INT -PK - - -entity_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name? -NVARCHAR(255) - - -street? -NVARCHAR(255) - - -streetnumber? -NVARCHAR(50) - - -zip? -NVARCHAR(50) - - -city? -NVARCHAR(255) - - -county? -NVARCHAR(255) - - -addresstype -NVARCHAR(50) - - -sort -INT + + + +entityaddress + + +id +INT +PK + + +entity_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name? +NVARCHAR(255) + + +street? +NVARCHAR(255) + + +streetnumber? +NVARCHAR(50) + + +zip? +NVARCHAR(50) + + +city? +NVARCHAR(255) + + +county? +NVARCHAR(255) + + +addresstype +NVARCHAR(50) + + +sort +INT - + entity - - - - - - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -number? -NVARCHAR(100) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +number? +NVARCHAR(100) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 -entityaddress:sw->entity:n +entityaddress:ne->entity:n - - + + + + + + + +customergroup:w->company:n + + + -entity:w->customergroup:n +entity:se->customergroup:n - - - - - - - -companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - -writeonly -BIT - - - -companysetting:se->company:n - - - + + entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - -sort -INT + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + +sort +INT -entitycontact:se->entity:n +entitycontact:sw->entity:n - - - - - - - -companyuser:e->company:n - - - + + - + google_auth - - - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 + + + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 - - -companyuser:w->google_auth:n - - - + + +google_auth:w->company:n + + + entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) -entityhistory:w->entity:n +entityhistory:sw->entity:n - - + + -entityhistory:se->companyuser:n +entityhistory:sw->companyuser:n - - + + - - -google_auth:w->company:n - - - + + +companyuser:w->company:n + + + - - -categorytext - - -categorytext - - -id -INT -PK - - -category_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) - - - -categorytext:e->language:n - - - - - - - - -categorytext:w->category:n - - - - - - - - -articletext - - -articletext - - -id -INT -PK - - -article_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) - - - -articletext:w->language:n - - - - - - - - -article - - - - - - - - - - - - - - - -article - - -id -INT -PK - - -company_id -INT -FK - - -parentattributename_id -? -INT -FK - - -parentarticle_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -unit_id -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -track_stock -BIT - - -is_parent -BIT - - -parentsort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 - - - -articletext:e->article:n - - - + + +companyuser:e->google_auth:n + + + - + unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +language_id +INT +FK + + +unit_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) -unittext:e->language:n +unittext:w->language:n - - + + unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT -unittext:se->unit:n +unittext:sw->unit:n - - + + - + + +articletext + + +articletext + + +id +INT +PK + + +language_id +INT +FK + + +article_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +articletext:w->language:n + + + + + + + + +article + + + + + + + + + + + + + + + +article + + +id +INT +PK + + +unit_id +INT +FK + + +brand_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +parentarticle_id +? +INT +FK + + +parentattributename_id +? +INT +FK + + +company_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +track_stock +BIT + + +is_parent +BIT + + +parentsort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +articletext:e->article:n + + + + + + + + +categorytext + + +categorytext + + +id +INT +PK + + +language_id +INT +FK + + +category_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +categorytext:e->language:n + + + + + + + + +categorytext:e->category:n + + + + + + + -categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) - + + +brand:w->company:n + + + + + + + + +articlelist + + + + +articlelist + + +id +INT +PK + + + +articlelistarticle + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT + + + +articlelistarticle:sw->articlelist:n + + + + + + + + +articlelistarticle:e->article:n + + + + + + + + +optionslist + + + + +optionslist + + +id +INT +PK + + + +optionslistarticle + + +optionslistarticle + + +id +INT +PK + + +optionslist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT + + + +optionslistarticle:sw->optionslist:n + + + + + + + -categoryarticle:se->category:n - - - - - - - - -categoryarticle:sw->article:n - - - - - - - - -category:e->category:e - - - +optionslistarticle:e->article:n + + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -companyuser_id -? -INT -FK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - -meta? -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + +meta? +NVARCHAR(MAX) articlehistory:e->companyuser:n - - + + -articlehistory:sw->article:n +articlehistory:se->article:n - - + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:se->article:n + + + @@ -1147,8 +1296,35 @@ article:w->company:n - - + + + + + + + +article:w->brand:n + + + + + + + + +article:w->articlelist:n + + + + + + + + +article:w->optionslist:n + + + @@ -1156,90 +1332,8 @@ article:e->article:e - - - - - - - -articlelist - - - - -articlelist - - -id -INT -PK - - - -article:w->articlelist:n - - - - - - - - -optionslist - - - - -optionslist - - -id -INT -PK - - - -article:w->optionslist:n - - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:w->brand:n - - - + + @@ -1247,49 +1341,49 @@ article:e->unit:n - - + + - + attributename - - - - - -attributename - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -exclusive -BIT + + + + + +attributename + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +exclusive +BIT article:se->attributename:n - - + + @@ -1297,1568 +1391,1476 @@ article:se->attributename:n - - - - - - - -articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) - - - -articleean:se->article:n - - - + + - + articlesupplier - - -articlesupplier - - -id -INT -PK - - -entity_id -INT -FK - - -tax_id -? -INT -FK - - -article_id -INT -FK - - -net? -FLOAT - - -sku? -NVARCHAR(100) - - -stock? -FLOAT - - -pu -FLOAT - - -sort -INT + + +articlesupplier + + +id +INT +PK + + +tax_id +? +INT +FK + + +article_id +INT +FK + + +entity_id +INT +FK + + +net? +FLOAT + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + +sort +INT - + articlesupplier:e->entity:n - - - + + + - -articlesupplier:se->article:n - - - + +articlesupplier:sw->article:n + + + - -articlesupplier:e->tax:n - - - + +articlesupplier:w->tax:n + + + - - -articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT - - - -articlelistarticle:w->article:n - - - + + +category:e->category:e + + + - - -articlelistarticle:se->articlelist:n - - - + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK + + + +categoryarticle:sw->article:n + + + - - -optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT - - - -optionslistarticle:e->article:n - - - - - - - - -optionslistarticle:sw->optionslist:n - - - - - - - - -brand:e->company:n - - - + + +categoryarticle:nw->category:n + + + pic - - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -smallthumbw -INT - - -smallthumbh -INT - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) - - -smallthumbbinary -VARBINARY(MAX) - - - -attributevaluepic - - -attributevaluepic - - -id -INT -PK - - -attributevalue_id -INT -FK - - -pic_id -INT -FK - - -sort -INT - - - -attributevaluepic:w->pic:n - - - - - - - - -attributevalue - - - - - -attributevalue - - -id -INT -PK - - -attribute_id -INT -FK - - -value -NVARCHAR(255) - - -sort -INT - - - -attributevaluepic:e->attributevalue:n - - - - - - - - -categorypic - - -categorypic - - -id -INT -PK - - -category_id -INT -FK - - -pic_id -INT -FK - - -sort -INT - - - -categorypic:sw->category:n - - - - - - - - -categorypic:e->pic:n - - - - - - - - -articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT - - - -articlepic:sw->article:n - - - - - - - - -articlepic:e->pic:n - - - - - + + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +smallthumbw +INT + + +smallthumbh +INT + + +smallthumbbinary +VARBINARY(MAX) + + +source +NVARCHAR(MAX) - + brandpic - - -brandpic - - -id -INT -PK - - -pic_id -INT -FK - - -brand_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - -brandpic:sw->brand:n - - - + +brandpic:e->brand:n + + + - + brandpic:w->pic:n - - - + + + + + + + + +articlepic + + +articlepic + + +id +INT +PK + + +pic_id +INT +FK + + +article_id +INT +FK + + +sort +INT + + + +articlepic:se->article:n + + + + + + + + +articlepic:w->pic:n + + + + + + + + +categorypic + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT + + + +categorypic:sw->category:n + + + + + + + + +categorypic:w->pic:n + + + + + + + + +attributevaluepic + + +attributevaluepic + + +id +INT +PK + + +pic_id +INT +FK + + +attributevalue_id +INT +FK + + +sort +INT + + + +attributevaluepic:w->pic:n + + + + + + + + +attributevalue + + + + + +attributevalue + + +id +INT +PK + + +attribute_id +INT +FK + + +value +NVARCHAR(255) + + +sort +INT + + + +attributevaluepic:w->attributevalue:n + + + file - - - -file - - -id -INT -PK - - -hash -NVARCHAR(64) - - -mimetype -NVARCHAR(255) - - -name -NVARCHAR(255) - - -size -BIGINT - - -created -DATETIME2 - - -modified -DATETIME2 - - -content -VARBINARY(MAX) - - -thumbavif? -VARBINARY(MAX) - - -thumbx? -INT - - -thumby? -INT - - -meta? -NVARCHAR(MAX) + + + +file + + +id +INT +PK + + +hash +NVARCHAR(64) + + +mimetype +NVARCHAR(255) + + +name +NVARCHAR(255) + + +size +BIGINT + + +created +DATETIME2 + + +modified +DATETIME2 + + +content +VARBINARY(MAX) + + +thumbavif? +VARBINARY(MAX) + + +thumbx? +INT + + +thumby? +INT + + +meta? +NVARCHAR(MAX) - + -deliverysummary - - -deliverysummary - - -id -INT -PK - - -company_id -INT -FK - - -supplier_id -? -INT -FK - - -manifest_id -INT -FK - - -delivery_number -NVARCHAR(100) - - -external_number? -NVARCHAR(100) - - -delivery_date -DATETIME2 - - -created_at -DATETIME2 - - -supplier_name? -NVARCHAR(255) - - -positions -INT - - -gross_total -FLOAT - - -net_total -FLOAT - - -total_quantity -FLOAT - - -stock_positions -INT - - -booked_positions -INT - - -unbooked_positions -INT - - -stock_quantity -FLOAT - - -booked_quantity -FLOAT - - -unbooked_quantity -FLOAT - - -is_booked -TINYINT +articlewarehousesummary + + +articlewarehousesummary + + +id +INT +PK + + +article_id +INT +FK + + +warehouse_id +INT +FK + + +stock +FLOAT - - -deliverysummary:se->company:n - - - - - - - - -deliverysummary:se->entity:n - - - - - - - - -manifest - - - - - - - - - - -manifest - - -id -INT -PK - - -company_id -INT -FK - - -entity_id -? -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -external_number? -NVARCHAR(100) - - -date -DATETIME2 - - -created_at -DATETIME2 - - - -deliverysummary:w->manifest:n - - - - - - - - -ordersummary - - -ordersummary - - -id -INT -PK - - -company_id -INT -FK - - -entity_id -? -INT -FK - - -manifest_id -INT -FK - - -order_number -NVARCHAR(100) - - -order_date -DATETIME2 - - -created_at -DATETIME2 - - -customer_name? -NVARCHAR(255) - - -positions -INT - - -gross_total -FLOAT - - -paid_total -FLOAT - - -ordered_quantity -FLOAT - - -shipped_quantity -FLOAT - - -is_paid -TINYINT - - -is_shipped -TINYINT - - - -ordersummary:se->company:n - - - - - - - - -ordersummary:e->entity:n - - - - - - - - -ordersummary:w->manifest:n - - - - - - - - -quotesummary - - -quotesummary - - -id -INT -PK - - -company_id -INT -FK - - -entity_id -? -INT -FK - - -manifest_id -INT -FK - - -quote_number -NVARCHAR(100) - - -quote_date -DATETIME2 - - -created_at -DATETIME2 - - -customer_name? -NVARCHAR(255) - - -positions -INT - - -gross_total -FLOAT - - -total_quantity -FLOAT - - - -quotesummary:se->company:n - - - - - - - - -quotesummary:e->entity:n - - - - - - - - -quotesummary:w->manifest:n - - - - - - - - -shipmentsummary - - -shipmentsummary - - -id -INT -PK - - -company_id -INT -FK - - -entity_id -? -INT -FK - - -manifest_id -INT -FK - - -shipment_number -NVARCHAR(100) - - -shipment_date -DATETIME2 - - -created_at -DATETIME2 - - -order_id? -INT - - -order_number? -NVARCHAR(100) - - -customer_name? -NVARCHAR(255) - - -positions -INT - - -total_quantity -FLOAT - - -package_count -INT - - -carrier? -NVARCHAR(100) - - -tracking_code? -NVARCHAR(255) - - -state -NVARCHAR(50) - - - -shipmentsummary:se->company:n - - - - - - - - -shipmentsummary:e->entity:n - - - - - - - - -shipmentsummary:w->manifest:n - - - - - - - - -package - - -package - - -id -INT -PK - - -entityaddress_id -INT -FK - - -shipping_id -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -state -NVARCHAR(100) - - -details? -NVARCHAR(MAX) - - - -package:e->entityaddress:n - - - - - - - - -shipping - - - -shipping - - -id -INT -PK - - -carrier_id -INT -FK - - -manifest_id -INT -FK - - -number -NVARCHAR(100) - - -tracking_code? -NVARCHAR(255) - - -type -NVARCHAR(100) - - -state -NVARCHAR(100) - - - -package:e->shipping:n - - - - - - - - -manifestarticle - - - -manifestarticle - - -id -INT -PK - - -manifest_id -INT -FK - - -sku? -NVARCHAR(100) - - -name? -NVARCHAR(255) - - -pu -FLOAT - - -amount -FLOAT - - -net -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT - - -taxrate -FLOAT - - -shippable -TINYINT - - - -manifestarticle:sw->manifest:n - - - - - - - - -manifest:w->company:n - - - - - - - - -manifest:e->entity:n - - - - - - - - -manifestattachment - - -manifestattachment - - -id -INT -PK - - -manifest_id -INT -FK - - -file_id -INT -FK - - -sort -INT - - - -manifestattachment:w->file:n - - - - - - - - -manifestattachment:ne->manifest:n - - - - - - - - -shipping:sw->manifest:n - - - - - - - - -carrier - - - -carrier - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -has_packages -BIT - - -is_reverse -BIT - - - -shipping:e->carrier:n - - - + + +articlewarehousesummary:nw->article:n + + + - + warehouse - - - - -warehouse - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT + + + + +warehouse + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT - + + +articlewarehousesummary:e->warehouse:n + + + + + + + + +manifestarticle + + + +manifestarticle + + +id +INT +PK + + +manifest_id +INT +FK + + +sku? +NVARCHAR(100) + + +name? +NVARCHAR(255) + + +pu +FLOAT + + +amount +FLOAT + + +net +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT + + +taxrate +FLOAT + + +shippable +TINYINT + + + +manifest + + + + + + + + + + + + +manifest + + +id +INT +PK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +date +DATETIME2 + + +created_at +DATETIME2 + + -warehouse:e->company:n - - - +manifestarticle:se->manifest:n + + + + + + + + +manifest:e->company:n + + + + + + + + +manifest:e->entity:n + + + + + + + + +manifestattachment + + +manifestattachment + + +id +INT +PK + + +file_id +INT +FK + + +manifest_id +INT +FK + + +sort +INT + + + +manifestattachment:e->file:n + + + + + + + + +manifestattachment:nw->manifest:n + + + + + + + + +shipping + + + +shipping + + +id +INT +PK + + +manifest_id +INT +FK + + +carrier_id +INT +FK + + +number +NVARCHAR(100) + + +tracking_code? +NVARCHAR(255) + + +type +NVARCHAR(100) + + +state +NVARCHAR(100) + + + +shipping:nw->manifest:n + + + + + + + + +carrier + + + +carrier + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +has_packages +BIT + + +is_reverse +BIT + + + +shipping:se->carrier:n + + + - + ledger - - -ledger - - -id -INT -PK - - -manifestarticle_id -INT -FK - - -warehouse_id -? -INT -FK - - -transaction_type -NVARCHAR(100) - - -amount -FLOAT - - -transaction_text -NVARCHAR(MAX) - - -hash_version -INT - - -prev_hash? -NVARCHAR(128) - - -created_at -DATETIME2 + + +ledger + + +id +INT +PK + + +manifestarticle_id +INT +FK + + +warehouse_id +? +INT +FK + + +transaction_type +NVARCHAR(100) + + +amount +FLOAT + + +transaction_text +NVARCHAR(MAX) + + +hash_version +INT + + +prev_hash? +NVARCHAR(128) + + +created_at +DATETIME2 - + ledger:w->manifestarticle:n - - - + + + - + ledger:sw->warehouse:n - - - + + + - - -articlewarehousesummary - - -articlewarehousesummary - - -id -INT -PK - - -warehouse_id -INT -FK - - -article_id -INT -FK - - -stock -FLOAT + + +warehouse:se->company:n + + + + - + + + +deliverysummary + + +deliverysummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +supplier_id +? +INT +FK + + +delivery_number +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +delivery_date +DATETIME2 + + +created_at +DATETIME2 + + +supplier_name? +NVARCHAR(255) + + +positions +INT + + +gross_total +FLOAT + + +net_total +FLOAT + + +total_quantity +FLOAT + + +stock_positions +INT + + +booked_positions +INT + + +unbooked_positions +INT + + +stock_quantity +FLOAT + + +booked_quantity +FLOAT + + +unbooked_quantity +FLOAT + + +is_booked +TINYINT + + + +deliverysummary:nw->company:n + + + + + + + -articlewarehousesummary:sw->article:n - - - +deliverysummary:w->entity:n + + + - - -articlewarehousesummary:se->warehouse:n - - - + + +deliverysummary:e->manifest:n + + + - - -payment - - -payment - - -id -INT -PK - - -manifest_id -INT -FK - - -account_id -INT -FK - - -type -NVARCHAR(100) - - -amount -FLOAT + + +package + + +package + + +id +INT +PK + + +shipping_id +INT +FK + + +entityaddress_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +state +NVARCHAR(100) + + +details? +NVARCHAR(MAX) - - -payment:e->manifest:n - - - + + +package:e->entityaddress:n + + + + + + + + +package:w->shipping:n + + + + + + + + +ordersummary + + +ordersummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +order_number +NVARCHAR(100) + + +order_date +DATETIME2 + + +created_at +DATETIME2 + + +customer_name? +NVARCHAR(255) + + +positions +INT + + +gross_total +FLOAT + + +paid_total +FLOAT + + +ordered_quantity +FLOAT + + +shipped_quantity +FLOAT + + +is_paid +TINYINT + + +is_shipped +TINYINT + + + +ordersummary:sw->company:n + + + + + + + + +ordersummary:w->entity:n + + + + + + + + +ordersummary:e->manifest:n + + + + + + + + +quotesummary + + +quotesummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +quote_number +NVARCHAR(100) + + +quote_date +DATETIME2 + + +created_at +DATETIME2 + + +customer_name? +NVARCHAR(255) + + +positions +INT + + +gross_total +FLOAT + + +total_quantity +FLOAT + + + +quotesummary:se->company:n + + + + + + + + +quotesummary:e->entity:n + + + + + + + + +quotesummary:e->manifest:n + + + + + + + + +shipmentsummary + + +shipmentsummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +shipment_number +NVARCHAR(100) + + +shipment_date +DATETIME2 + + +created_at +DATETIME2 + + +order_id? +INT + + +order_number? +NVARCHAR(100) + + +customer_name? +NVARCHAR(255) + + +positions +INT + + +total_quantity +FLOAT + + +package_count +INT + + +carrier? +NVARCHAR(100) + + +tracking_code? +NVARCHAR(255) + + +state +NVARCHAR(50) + + + +shipmentsummary:ne->company:n + + + + + + + + +shipmentsummary:e->entity:n + + + + + + + + +shipmentsummary:w->manifest:n + + + - + account - - - -account - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -name -NVARCHAR(255) - - - -payment:w->account:n - - - - - + + + +account + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +name +NVARCHAR(255) - + account:w->company:n - - - + + + - + price - - -price - - -id -INT -PK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -article_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + +price + + +id +INT +PK + + +tax_id +INT +FK + + +article_id +INT +FK + + +customergroup_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT - + price:e->customergroup:n - - - + + + - -price:nw->article:n - - - + +price:sw->article:n + + + - + price:w->tax:n - - - + + + + + + + + +payment + + +payment + + +id +INT +PK + + +account_id +INT +FK + + +manifest_id +INT +FK + + +type +NVARCHAR(100) + + +amount +FLOAT + + +created_at +DATETIME2 + + + +payment:e->manifest:n + + + + + + + + +payment:w->account:n + + + @@ -2866,919 +2868,1172 @@ unit:e->company:n - - - - - - - -printer - - -printer - - -id -INT -PK - - -company_id -INT -FK - - -address -NVARCHAR(255) - - -type -NVARCHAR(50) - - -model? -NVARCHAR(255) - - -serial? -NVARCHAR(255) - - -sort -INT - - -desc? -NVARCHAR(MAX) - - - -printer:w->company:n - - - - - - - - -labeltemplate - - -labeltemplate - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -width -INT - - -height -INT - - -dpi -INT - - -type -NVARCHAR(50) - - -content -NVARCHAR(MAX) - - -thumb? -VARBINARY(MAX) - - - -labeltemplate:e->company:n - - - + + - + posterminalobituary - - -posterminalobituary - - -id -INT -PK - - -posterminal_id -INT -FK - - -entity_type -NVARCHAR(50) - - -entity_id -INT - - -created_at -DATETIME2 + + +posterminalobituary + + +id +INT +PK + + +posterminal_id +INT +FK + + +entity_type +NVARCHAR(50) + + +entity_id +INT + + +created_at +DATETIME2 - + posterminal - - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -root_category_id -? -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin? -NVARCHAR(50) - - -paired_at? -DATETIME2 - - -clientserial? -NVARCHAR(255) - - -maxknownobituaryid? -INT - - -sync_root_articles -BIT - - -lastseen_at? -DATETIME2 - - -updated_at -DATETIME2 + + + + + +posterminal + + +id +INT +PK + + +root_category_id +? +INT +FK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin? +NVARCHAR(50) + + +paired_at? +DATETIME2 + + +clientserial? +NVARCHAR(255) + + +maxknownobituaryid? +INT + + +sync_root_articles +BIT + + +lastseen_at? +DATETIME2 + + +updated_at +DATETIME2 -posterminalobituary:se->posterminal:n +posterminalobituary:ne->posterminal:n - - + + - -posterminal:e->company:n - - - + +posterminal:w->company:n + + + - + posterminal:w->category:n - - - + + + + + + + + +posterminalsyncqueue + + +posterminalsyncqueue + + +id +INT +PK + + +posterminal_id +INT +FK + + +entity_type +NVARCHAR(50) + + +entity_id +INT + + + +posterminalsyncqueue:sw->posterminal:n + + + - + posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -posterminal_id -INT -FK - - -category_id -INT -FK + + +posterminalcategory + + +id +INT +PK + + +category_id +INT +FK + + +posterminal_id +INT +FK - -posterminalcategory:w->category:n - - - + +posterminalcategory:e->category:n + + + - -posterminalcategory:se->posterminal:n - - - + +posterminalcategory:sw->posterminal:n + + + - - -modelvfs - - -modelvfs - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -mime -NVARCHAR(100) - - -content -VARBINARY(MAX) - - -meta -NVARCHAR(MAX) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + +labeltemplate + + +labeltemplate + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +width +INT + + +height +INT + + +dpi +INT + + +type +NVARCHAR(50) + + +content +NVARCHAR(MAX) + + +thumb? +VARBINARY(MAX) - - -modelvfs:sw->company:n - - - + + +labeltemplate:w->company:n + + + + + + + + +printer + + +printer + + +id +INT +PK + + +company_id +INT +FK + + +address +NVARCHAR(255) + + +type +NVARCHAR(50) + + +model? +NVARCHAR(255) + + +serial? +NVARCHAR(255) + + +sort +INT + + +desc? +NVARCHAR(MAX) + + + +printer:w->company:n + + + subagentschedule - - -subagentschedule - - -id -INT -PK - - -company_id -INT -FK - - -desc? -NVARCHAR(MAX) - - -prompt -NVARCHAR(MAX) - - -skills? -NVARCHAR(MAX) - - -lastagentnote? -NVARCHAR(MAX) - - -max_itr -INT - - -cron -NVARCHAR(100) - - -created_at -DATETIME2 - - -lastrun_at? -DATETIME2 + + +subagentschedule + + +id +INT +PK + + +company_id +INT +FK + + +desc? +NVARCHAR(MAX) + + +prompt +NVARCHAR(MAX) + + +skills? +NVARCHAR(MAX) + + +lastagentnote? +NVARCHAR(MAX) + + +max_itr +INT + + +cron +NVARCHAR(100) + + +created_at +DATETIME2 + + +lastrun_at? +DATETIME2 - -subagentschedule:sw->company:n - - - + +subagentschedule:w->company:n + + + modelconversation - - - -modelconversation - - -id -INT -PK - - -companyuser_id -INT -FK - - -provider -NVARCHAR(100) - - -model -NVARCHAR(255) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 - - -state -NVARCHAR(20) - - -started_by? -INT + + + +modelconversation + + +id +INT +PK + + +companyuser_id +INT +FK + + +provider +NVARCHAR(100) + + +model +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + +state +NVARCHAR(20) + + +started_by? +INT - + modelconversation:w->companyuser:n - - - + + + modelmessage - - -modelmessage - - -id -INT -PK - - -conversation_id -INT -FK - - -created_at -DATETIME2 - - -role -NVARCHAR(20) - - -message -NVARCHAR(MAX) + + +modelmessage + + +id +INT +PK + + +conversation_id +INT +FK + + +created_at +DATETIME2 + + +role +NVARCHAR(20) + + +message +NVARCHAR(MAX) + +modelmessage:sw->modelconversation:n + + + + + + + + +modelvfs + + +modelvfs + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +mime +NVARCHAR(100) + + +content +VARBINARY(MAX) + + +meta +NVARCHAR(MAX) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + -modelmessage:se->modelconversation:n - - - +modelvfs:w->company:n + + + - - -attributevalue:se->attributename:n - - - - + + +articleproperty + + +articleproperty + + +id +INT +PK + + +article_id +INT +FK + + +property_id +INT +FK + + +value +NVARCHAR(MAX) + + +sort +INT - - - -attributename:e->company:n - - - + + +articleproperty:w->article:n + + + - + propertyname - - - - -propertyname - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -exclusive -BIT + + + + +propertyname + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +exclusive +BIT - - -propertyname:e->company:n - - - + + +articleproperty:e->propertyname:n + + + + + + + + +articleattributevalue + + +articleattributevalue + + +id +INT +PK + + +article_id +INT +FK + + +attributevalue_id +INT +FK + + +sort +INT + + + +articleattributevalue:e->article:n + + + + + + + + +articleattributevalue:w->attributevalue:n + + + categoryproperty - - -categoryproperty - - -id -INT -PK - - -property_id -INT -FK - - -category_id -INT -FK - - -value -NVARCHAR(MAX) - - -sort -INT + + +categoryproperty + + +id +INT +PK + + +category_id +INT +FK + + +property_id +INT +FK + + +value +NVARCHAR(MAX) + + +sort +INT - + categoryproperty:w->category:n - - - + + + - + categoryproperty:e->propertyname:n - - - + + + categoryattributevalue - - -categoryattributevalue - - -id -INT -PK - - -attributevalue_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categoryattributevalue + + +id +INT +PK + + +category_id +INT +FK + + +attributevalue_id +INT +FK + + +sort +INT - + categoryattributevalue:w->category:n - - - + + + - + categoryattributevalue:e->attributevalue:n - - - + + + - - -articleproperty - - -articleproperty - - -id -INT -PK - - -property_id -INT -FK - - -article_id -INT -FK - - -value -NVARCHAR(MAX) - - -sort -INT - - + -articleproperty:w->article:n - - - +propertyname:e->company:n + + + - - -articleproperty:e->propertyname:n - - - + + +attributename:w->company:n + + + - - -articleattributevalue - - -articleattributevalue - - -id -INT -PK - - -attributevalue_id -INT -FK - - -article_id -INT -FK - - -sort -INT - - + -articleattributevalue:e->article:n - - - +attributevalue:se->attributename:n + + + - - -articleattributevalue:e->attributevalue:n - - - + + +returnsummary + + +returnsummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +supplier_id +? +INT +FK + + +return_number +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +return_date +DATETIME2 + + +created_at +DATETIME2 + + +supplier_name? +NVARCHAR(255) + + +positions +INT + + +gross_total +FLOAT + + +net_total +FLOAT + + +total_quantity +FLOAT + + +stock_positions +INT + + +booked_positions +INT + + +unbooked_positions +INT + + +stock_quantity +FLOAT + + +booked_quantity +FLOAT + + +unbooked_quantity +FLOAT + + +is_booked +TINYINT + + + +returnsummary:e->company:n + + + + + + + + +returnsummary:w->entity:n + + + + + + + + +returnsummary:w->manifest:n + + + + + + + + +retouresummary + + +retouresummary + + +id +INT +PK + + +manifest_id +INT +FK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +retoure_number +NVARCHAR(100) + + +retoure_date +DATETIME2 + + +created_at +DATETIME2 + + +customer_name? +NVARCHAR(255) + + +positions +INT + + +gross_total +FLOAT + + +paid_total +FLOAT + + +ordered_quantity +FLOAT + + +shipped_quantity +FLOAT + + +is_paid +TINYINT + + +is_shipped +TINYINT + + + +retouresummary:w->company:n + + + + + + + + +retouresummary:e->entity:n + + + + + + + + +retouresummary:e->manifest:n + + + - -carrier:w->company:n - - - - - - - - -mcp_auth_code - - -mcp_auth_code - - -code -NVARCHAR(128) -PK - - -google_auth_id -INT -FK - - -client_id -NVARCHAR(100) - - -redirect_uri -NVARCHAR(2048) - - -code_challenge -NVARCHAR(255) - - -code_challenge_method -NVARCHAR(32) - - -scope? -NVARCHAR(255) - - -expires_at -DATETIME2 - - -used -TINYINT - - -created_at -DATETIME2 - - - -mcp_auth_code:se->google_auth:n - - - - - - - - -mcp_token - - -mcp_token - - -id -INT -PK - - -google_auth_id -INT -FK - - -refresh_token -NVARCHAR(128) - - -client_id -NVARCHAR(100) - - -scope? -NVARCHAR(255) - - -expires_at -DATETIME2 - - -revoked -TINYINT - - -created_at -DATETIME2 - - - -mcp_token:w->google_auth:n - - - + +carrier:e->company:n + + + - + mcp_client - - -mcp_client - - -id -INT -PK - - -client_id -NVARCHAR(100) - - -client_secret? -NVARCHAR(255) - - -client_name -NVARCHAR(255) - - -redirect_uris -NVARCHAR(MAX) - - -grant_types? -NVARCHAR(255) - - -response_types? -NVARCHAR(255) - - -scope? -NVARCHAR(255) - - -created_at -DATETIME2 + + +mcp_client + + +id +INT +PK + + +client_id +NVARCHAR(100) + + +client_secret? +NVARCHAR(255) + + +client_name +NVARCHAR(255) + + +redirect_uris +NVARCHAR(MAX) + + +grant_types? +NVARCHAR(255) + + +response_types? +NVARCHAR(255) + + +scope? +NVARCHAR(255) + + +created_at +DATETIME2 - + healthcheck - - -healthcheck - - -id -INT -PK - - -sequence -BIGINT - - -updated_at -DATETIME2 + + +healthcheck + + +id +INT +PK + + +sequence +BIGINT + + +updated_at +DATETIME2 + + + +mcp_auth_code + + +mcp_auth_code + + +code +NVARCHAR(128) +PK + + +google_auth_id +INT +FK + + +client_id +NVARCHAR(100) + + +redirect_uri +NVARCHAR(2048) + + +code_challenge +NVARCHAR(255) + + +code_challenge_method +NVARCHAR(32) + + +scope? +NVARCHAR(255) + + +expires_at +DATETIME2 + + +used +TINYINT + + +created_at +DATETIME2 + + + +mcp_auth_code:sw->google_auth:n + + + + + + + + +mcp_token + + +mcp_token + + +id +INT +PK + + +google_auth_id +INT +FK + + +refresh_token +NVARCHAR(128) + + +client_id +NVARCHAR(100) + + +scope? +NVARCHAR(255) + + +expires_at +DATETIME2 + + +revoked +TINYINT + + +created_at +DATETIME2 + + + +mcp_token:w->google_auth:n + + + + + - + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/scripts/test-syncqueue.js b/scripts/test-syncqueue.js new file mode 100644 index 0000000..781d5af --- /dev/null +++ b/scripts/test-syncqueue.js @@ -0,0 +1,436 @@ +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 posterminalsyncqueue table and sync queue flow ---'); + + const tempDir = mkdtempSync(join(tmpdir(), 'test-syncqueue-')); + 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); + + const clearQueue = () => { + provider.db.prepare('DELETE FROM posterminalsyncqueue').run(); + }; + + const getQueue = () => + provider.db + .prepare('SELECT posterminal_id, entity_type, entity_id FROM posterminalsyncqueue ORDER BY id ASC') + .all(); + + try { + // 1. Verify posterminalsyncqueue table exists and has expected columns + console.log('1. Checking posterminalsyncqueue schema...'); + const tableInfo = provider.db.pragma('table_info("posterminalsyncqueue")'); + const colNames = tableInfo.map((c) => c.name); + console.log('posterminalsyncqueue columns:', colNames); + assert.deepEqual(colNames, [ + 'id', + 'posterminal_id', + 'entity_type', + 'entity_id', + ]); + + // 2. Setup company & 2 POS terminals + another company with 1 terminal (to test company scoping) + console.log('2. Setting up base data with 2 terminals in company 1 and 1 terminal in company 2...'); + const googleAuthId = await store.google_auth.add('syncqueue-tester@example.com'); + const company1 = await store.company.create({ + name: 'SyncQueue Corp 1', + iso639: 'de', + taxrate: 19, + reducedtaxrate: 7, + googleAuthId, + }); + const company2 = await store.company.create({ + name: 'SyncQueue Corp 2', + iso639: 'de', + taxrate: 19, + reducedtaxrate: 7, + googleAuthId, + }); + + await store.google_auth.setSelectedCompany(googleAuthId, company2.id); + const otherTerminal = await store.posterminal.upsert(googleAuthId, { + name: 'Other Corp Terminal', + pairingpin: '9999', + }); + + await store.google_auth.setSelectedCompany(googleAuthId, company1.id); + const term1 = await store.posterminal.upsert(googleAuthId, { + name: 'POS Terminal 1', + pairingpin: '1111', + }); + const term2 = await store.posterminal.upsert(googleAuthId, { + name: 'POS Terminal 2', + pairingpin: '2222', + }); + + // Helper to assert that both term1 and term2 received the expected entity_type + entity_id, and otherTerminal did not + const assertQueuedForBothTerminals = (entityType, entityId, label) => { + const rows = getQueue(); + const matching = rows.filter((r) => r.entity_type === entityType && r.entity_id === entityId); + const termIds = matching.map((r) => r.posterminal_id).sort((a, b) => a - b); + assert.deepEqual( + termIds, + [term1.id, term2.id], + `Expected [${term1.id}, ${term2.id}] for ${label} (${entityType}:${entityId}), got ${JSON.stringify(rows)}`, + ); + const otherRows = rows.filter((r) => r.posterminal_id === otherTerminal.id); + assert.equal(otherRows.length, 0, `Other company terminal should not receive rows (${label})`); + }; + + // 3. Test Category operations + console.log('3. Testing category operations...'); + clearQueue(); + const catA = await store.category.createUnderParent({ + googleAuthId, + parentId: company1.rootCategoryId, + name: 'Category A', + }); + assertQueuedForBothTerminals('category', catA.id, 'category.createUnderParent'); + + clearQueue(); + await store.category.rename({ + googleAuthId, + id: catA.id, + name: 'Category A Renamed', + }); + assertQueuedForBothTerminals('category', catA.id, 'category.rename'); + + clearQueue(); + const catLeafId = await store.category.ensureByPath({ + googleAuthId, + parentId: company1.rootCategoryId, + path: 'Category B | Subcategory B1', + }); + assertQueuedForBothTerminals('category', catLeafId, 'category.ensureByPath'); + + const catBRow = provider.db + .prepare('SELECT c.id FROM category c WHERE c.parent_id = ? AND c.id != ?') + .get(company1.rootCategoryId, catA.id); + clearQueue(); + await store.category.order({ + googleAuthId, + parentId: company1.rootCategoryId, + categoryIds: [catBRow.id, catA.id], + }); + assertQueuedForBothTerminals('category', catA.id, 'category.order'); + assertQueuedForBothTerminals('category', catBRow.id, 'category.order (sibling)'); + + clearQueue(); + await store.category.move({ + googleAuthId, + id: catLeafId, + parentId: catA.id, + }); + assertQueuedForBothTerminals('category', catLeafId, 'category.move'); + + const prop = await store.propertyname.create(googleAuthId, { name: 'Color', exclusive: false }); + clearQueue(); + await store.categoryproperty.setByCategoryId(googleAuthId, catA.id, [ + { propertyId: prop.id, value: 'Red' }, + ]); + assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.setByCategoryId'); + + clearQueue(); + await store.categoryproperty.delete(googleAuthId, catA.id, prop.id); + assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.delete'); + + const attr = await store.attributename.create(googleAuthId, { name: 'Size', exclusive: true }); + const attrVal = await store.attributevalue.create(googleAuthId, attr.id, { value: 'XL' }); + clearQueue(); + await store.categoryattributevalue.setByCategoryId(googleAuthId, catA.id, [attrVal.id]); + assertQueuedForBothTerminals('category', catA.id, 'categoryattributevalue.setByCategoryId'); + + // 4. Test Article operations + console.log('4. Testing article operations...'); + clearQueue(); + const art1 = await store.article.createUnderParent({ + googleAuthId, + parentId: catA.id, + name: 'Cola 0.33L', + }); + assertQueuedForBothTerminals('article', art1.id, 'article.createUnderParent'); + + clearQueue(); + await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic 0.33L' }); + assertQueuedForBothTerminals('article', art1.id, 'article.updateBySku'); + + clearQueue(); + await store.article.setPricesBySku(googleAuthId, art1.sku, [{ net: 1.5, taxRate: 19 }]); + assertQueuedForBothTerminals('article', art1.id, 'article.setPricesBySku'); + + clearQueue(); + await store.article.setEansBySku(googleAuthId, art1.sku, ['4001234567890']); + assertQueuedForBothTerminals('article', art1.id, 'article.setEansBySku'); + + const brand = await store.brand.create(googleAuthId, { name: 'ColaBrand' }); + clearQueue(); + await store.article.setBrandIdBySku(googleAuthId, art1.sku, brand.id); + assertQueuedForBothTerminals('article', art1.id, 'article.setBrandIdBySku'); + + clearQueue(); + await store.brand.update(googleAuthId, brand.id, { name: 'ColaBrand Updated' }); + assertQueuedForBothTerminals('article', art1.id, 'brand.update'); + + clearQueue(); + await store.article.setWeightBySku(googleAuthId, art1.sku, 0.35); + assertQueuedForBothTerminals('article', art1.id, 'article.setWeightBySku'); + + const wh = await store.warehouse.create(googleAuthId, { name: 'Main Warehouse' }); + clearQueue(); + await store.article.setTrackStockBySku(googleAuthId, art1.sku, true); + assertQueuedForBothTerminals('article', art1.id, 'article.setTrackStockBySku'); + + clearQueue(); + await store.stock.adjust(googleAuthId, { + sku: art1.sku, + warehouseId: wh.id, + amount: 10, + text: 'Initial stock', + }); + assertQueuedForBothTerminals('article', art1.id, 'stock.adjust'); + + clearQueue(); + await store.article.setCategoryIdsBySku(googleAuthId, art1.sku, [catA.id, catLeafId]); + assertQueuedForBothTerminals('article', art1.id, 'article.setCategoryIdsBySku'); + + clearQueue(); + await store.articleproperty.setByArticleSku(googleAuthId, art1.sku, [ + { propertyId: prop.id, value: 'Dark' }, + ]); + assertQueuedForBothTerminals('article', art1.id, 'articleproperty.setByArticleSku'); + + clearQueue(); + await store.articleproperty.delete(googleAuthId, art1.sku, prop.id); + assertQueuedForBothTerminals('article', art1.id, 'articleproperty.delete'); + + clearQueue(); + await store.articleattributevalue.setByArticleSku(googleAuthId, art1.sku, [attrVal.id]); + assertQueuedForBothTerminals('article', art1.id, 'articleattributevalue.setByArticleSku'); + + clearQueue(); + await store.attributevalue.update(googleAuthId, attrVal.id, { value: 'XXL' }); + assertQueuedForBothTerminals('article', art1.id, 'attributevalue.update'); + + // Optionslist & Stückliste + const pfand = await store.article.createUnderParent({ + googleAuthId, + parentId: catA.id, + name: 'Pfand 0.25', + }); + clearQueue(); + await store.optionslist.setOptionsByArticleSku(googleAuthId, art1.sku, [ + { sku: pfand.sku, count: 1, mandated: true }, + ]); + assertQueuedForBothTerminals('article', art1.sku ? art1.id : 0, 'optionslist.setOptionsByArticleSku'); + + // Updating option article price should also enqueue parent article! + clearQueue(); + await store.article.setPricesBySku(googleAuthId, pfand.sku, [{ net: 0.25, taxRate: 19 }]); + assertQueuedForBothTerminals('article', pfand.id, 'setPricesBySku (option itself)'); + assertQueuedForBothTerminals('article', art1.id, 'setPricesBySku (option parent)'); + + const bundle = await store.article.createUnderParent({ + googleAuthId, + parentId: catA.id, + name: 'Cola Crate', + }); + clearQueue(); + await store.articlelist.setItemsByArticleSku(googleAuthId, bundle.sku, [ + { sku: art1.sku, count: 6 }, + ]); + assertQueuedForBothTerminals('article', bundle.id, 'articlelist.setItemsByArticleSku'); + + // Adjusting component stock should also enqueue Stückliste parent! + clearQueue(); + await store.stock.adjust(googleAuthId, { + sku: art1.sku, + warehouseId: wh.id, + amount: 2, + text: 'Restock component', + }); + assertQueuedForBothTerminals('article', art1.id, 'stock.adjust (component)'); + assertQueuedForBothTerminals('article', bundle.id, 'stock.adjust (Stückliste parent)'); + + // 5. Test Customer Group operations + console.log('5. Testing customergroup operations...'); + clearQueue(); + const cg1 = await store.customergroup.create(googleAuthId, { name: 'B2B' }); + assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.create'); + + clearQueue(); + await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Gold' }); + assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.update'); + + const allGroups = await store.customergroup.list(googleAuthId); + clearQueue(); + await store.customergroup.order(googleAuthId, { + customergroupIds: allGroups.map((g) => g.id).reverse(), + }); + assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.order'); + + // 6. Test Customer (Entity + Address + Contact) operations + console.log('6. Testing customer (entity, entityaddress, entitycontact) operations...'); + clearQueue(); + const cust = await store.entity.create(googleAuthId, { + customergroupId: cg1.id, + name: 'Doe', + firstName: 'John', + }); + assertQueuedForBothTerminals('customer', cust.id, 'entity.create'); + + clearQueue(); + await store.entity.update(googleAuthId, cust.id, { firstName: 'Johnny' }); + assertQueuedForBothTerminals('customer', cust.id, 'entity.update'); + + clearQueue(); + const addr = await store.entityaddress.create(googleAuthId, cust.id, { + addressType: 'billing', + street: 'Main St 1', + city: 'Berlin', + }); + assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.create'); + + clearQueue(); + await store.entityaddress.update(googleAuthId, cust.id, addr.id, { + city: 'Munich', + }); + assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.update'); + + clearQueue(); + await store.entityaddress.delete(googleAuthId, cust.id, addr.id); + assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.delete'); + + clearQueue(); + const contact = await store.entitycontact.create(googleAuthId, cust.id, { + contactType: 'email', + contact: 'john@example.com', + }); + assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.create'); + + clearQueue(); + await store.entitycontact.update(googleAuthId, cust.id, contact.id, { + contact: 'johnny@example.com', + }); + assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.update'); + + clearQueue(); + await store.entitycontact.delete(googleAuthId, cust.id, contact.id); + assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.delete'); + + // 7. Test CHECK constraint on entity_type + console.log('7. Testing entity_type CHECK constraint...'); + assert.throws(() => { + provider.db + .prepare('INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) VALUES (?, ?, ?)') + .run(term1.id, 'invalid_type', 1); + }, /CHECK constraint failed/); + + // 8. Test POS Sync Cursor (posterminalsyncqueue.id) across counts and lists + console.log('8. Testing POS Sync Cursor (posterminalsyncqueue.id) for counts & lists...'); + // Create a fresh terminal (term3) and enable all categories on it to verify full initial sync + delta cursor + const term3 = await store.posterminal.upsert(googleAuthId, { + name: 'POS Terminal 3', + pairingpin: '3333', + }); + await store.posterminal.setOnAll(googleAuthId, term3.id); + + // Initial sync with cursor = 0 + const initProductCount = await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: 0 }); + const initCategoryCount = await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: 0 }); + const initCustomerCount = await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: 0 }); + const initCustomerGroupCount = await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: 0 }); + const initCompositeCount = await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: 0 }); + + assert.ok(initProductCount > 0, `Expected initProductCount > 0, got ${initProductCount}`); + assert.ok(initCategoryCount > 0, `Expected initCategoryCount > 0, got ${initCategoryCount}`); + assert.ok(initCustomerCount > 0, `Expected initCustomerCount > 0, got ${initCustomerCount}`); + assert.ok(initCustomerGroupCount > 0, `Expected initCustomerGroupCount > 0, got ${initCustomerGroupCount}`); + assert.ok(initCompositeCount > 0, `Expected initCompositeCount > 0, got ${initCompositeCount}`); + + const products = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: 0, limit: 100 }); + const categories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: 0, limit: 100 }); + const customers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: 0, limit: 100 }); + const customerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: 0, limit: 100 }); + + assert.equal(products.length, initProductCount); + assert.equal(categories.length, initCategoryCount); + assert.equal(customers.length, initCustomerCount); + assert.equal(customerGroups.length, initCustomerGroupCount); + + // Advance cursors to max lastChanged + const maxProductCursor = Math.max(...products.map((p) => Number(p.lastChanged))); + const maxCategoryCursor = Math.max(...categories.map((c) => Number(c.lastChanged))); + const maxCustomerCursor = Math.max(...customers.map((c) => Number(c.lastChanged))); + const maxCustomerGroupCursor = Math.max(...customerGroups.map((cg) => Number(cg.lastChanged))); + + // Verify all counts and lists are 0 at the current cursors + assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0); + assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 0); + assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 0); + assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 0); + assert.equal(await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0); + + assert.equal((await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor })).length, 0); + assert.equal((await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor })).length, 0); + assert.equal((await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor })).length, 0); + assert.equal((await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor })).length, 0); + + // Now touch one article twice, one category, one customer, one customergroup + await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v2' }); + await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v3' }); + await store.category.rename({ googleAuthId, id: catA.id, name: 'Category A v2' }); + await store.entity.update(googleAuthId, cust.id, { firstName: 'John v2' }); + await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Platinum' }); + + // Even though art1 was updated twice, count should be 1 (deduplicated by entity_id with MAX(id) > cursor) + assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 1); + assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 1); + assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 1); + assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 1); + + const deltaProducts = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor }); + const deltaCategories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor }); + const deltaCustomers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor }); + const deltaCustomerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }); + + assert.equal(deltaProducts.length, 1); + assert.equal(deltaProducts[0]._id, String(art1.id)); + assert.ok(Number(deltaProducts[0].lastChanged) > maxProductCursor); + + assert.equal(deltaCategories.length, 1); + assert.equal(deltaCategories[0]._id, String(catA.id)); + assert.ok(Number(deltaCategories[0].lastChanged) > maxCategoryCursor); + + assert.equal(deltaCustomers.length, 1); + assert.equal(deltaCustomers[0].id, String(cust.id)); + assert.ok(Number(deltaCustomers[0].lastChanged) > maxCustomerCursor); + + assert.equal(deltaCustomerGroups.length, 1); + assert.equal(deltaCustomerGroups[0].customerGroupId, String(cg1.id)); + assert.ok(Number(deltaCustomerGroups[0].lastChanged) > maxCustomerGroupCursor); + + console.log('✅ ALL POSTERMINALSYNCQUEUE TESTS PASSED'); + } finally { + try { + rmSync(tempDir, { recursive: true, force: true }); + } catch {} + } +} + +run().catch((err) => { + console.error('Test failed:', err); + process.exit(1); +}); diff --git a/src/index.js b/src/index.js index ba150c9..6951714 100644 --- a/src/index.js +++ b/src/index.js @@ -143,6 +143,7 @@ import posterminalGetCompositeProductCount from './store/posterminal/getComposit import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js'; import posterminalGetDeletedEntityList from './store/posterminal/getDeletedEntityList.js'; import posterminalRecordObituary from './store/posterminal/recordObituary.js'; +import posterminalRecordSyncQueue from './store/posterminal/recordSyncQueue.js'; import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js'; import posterminalGetCategoryList from './store/posterminal/getCategoryList.js'; import posterminalGetProductList from './store/posterminal/getProductList.js'; @@ -528,6 +529,7 @@ export default class LibDashStore { getDeletedEntityCount: posterminalGetDeletedEntityCount(provider), getDeletedEntityList: posterminalGetDeletedEntityList(provider), recordObituary: posterminalRecordObituary(provider), + recordSyncQueue: posterminalRecordSyncQueue(provider), getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider), getCategoryList: posterminalGetCategoryList(provider), getProductList: posterminalGetProductList(provider), diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index 03f5cfb..b4c9eff 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -24,6 +24,8 @@ export const TABLES = [ 'modelconversation', 'companyuser', 'companysetting', + 'posterminalsyncqueue', + 'posterminalobituary', 'posterminalcategory', 'posterminal', 'printer', diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 59a807e..291d580 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -249,6 +249,38 @@ WHERE articlelist_id IN ( ) `; +export const ENQUEUE_SYNC_BY_ARTICLE = ` +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'article', a.id +FROM article a +INNER JOIN posterminal pt ON pt.company_id = a.company_id +WHERE a.id = @articleId +`; + +export const ENQUEUE_OPTION_PARENT_ARTICLES_SYNC = ` +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'article', a.id +FROM article a +INNER JOIN posterminal pt ON pt.company_id = a.company_id +WHERE a.optionslist_id IN ( + SELECT ola.optionslist_id + FROM optionslistarticle ola + WHERE ola.article_id = @articleId +) +`; + +export const ENQUEUE_STUECKLISTE_ARTICLES_SYNC = ` +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'article', a.id +FROM article a +INNER JOIN posterminal pt ON pt.company_id = a.company_id +WHERE a.articlelist_id IN ( + SELECT ala.articlelist_id + FROM articlelistarticle ala + WHERE ala.article_id = @articleId +) +`; + export const CREATE = ` INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock) diff --git a/src/providers/mssql/queries/category.js b/src/providers/mssql/queries/category.js index 1838bf6..36e16cf 100644 --- a/src/providers/mssql/queries/category.js +++ b/src/providers/mssql/queries/category.js @@ -240,3 +240,40 @@ WHERE id IN ( ) `; +export const ENQUEUE_SUBTREE_CATEGORIES_SYNC = ` +;WITH cats AS ( + SELECT id FROM category WHERE id = @id + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +) +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'category', cats.id +FROM cats +INNER JOIN posterminal pt ON pt.company_id = @companyId +`; + +export const ENQUEUE_SUBTREE_ARTICLES_SYNC = ` +;WITH cats AS ( + SELECT id FROM category WHERE id = @id + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +), +arts AS ( + SELECT ca.article_id AS id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + UNION + SELECT child.id FROM article child + WHERE child.parentarticle_id IN ( + SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + ) + UNION + SELECT parent.parentarticle_id AS id FROM article parent + WHERE parent.id IN ( + SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + ) AND parent.parentarticle_id IS NOT NULL +) +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'article', arts.id +FROM arts +INNER JOIN posterminal pt ON pt.company_id = @companyId +`; + diff --git a/src/providers/mssql/queries/entity.js b/src/providers/mssql/queries/entity.js index deba50d..03ec5c4 100644 --- a/src/providers/mssql/queries/entity.js +++ b/src/providers/mssql/queries/entity.js @@ -136,6 +136,12 @@ WHERE id = @id AND customergroup_id = @customergroupId `; +export const TOUCH_UPDATED_AT = ` +UPDATE entity +SET updated_at = SYSUTCDATETIME() +WHERE id = @id +`; + export const DELETE_BY_COMPANY_AND_ID = ` DELETE e FROM entity e diff --git a/src/providers/mssql/queries/posterminal.js b/src/providers/mssql/queries/posterminal.js index 303e78f..b2c1d8c 100644 --- a/src/providers/mssql/queries/posterminal.js +++ b/src/providers/mssql/queries/posterminal.js @@ -172,66 +172,100 @@ WHERE token = @token `; export const COUNT_PRODUCTS = ` -SELECT COUNT(DISTINCT a.id) AS count -FROM article a -INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id -WHERE EXISTS ( - SELECT 1 - FROM categoryarticle ca - INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id - WHERE ca.article_id = a.id - AND pc.posterminal_id = @posterminalId -) -AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor +SELECT COUNT(*) AS count +FROM ( + SELECT MAX(q.id) AS syncId, a.id + FROM posterminalsyncqueue q + INNER JOIN article a ON a.id = q.entity_id + INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = a.company_id + WHERE q.posterminal_id = @posterminalId + AND q.entity_type = 'article' + AND EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id + WHERE ca.article_id = a.id + AND pc.posterminal_id = @posterminalId + ) + GROUP BY a.id + HAVING MAX(q.id) > @cursor +) t `; export const COUNT_CATEGORIES = ` SELECT COUNT(*) AS count -FROM category c -INNER JOIN posterminalcategory pc ON pc.category_id = c.id -INNER JOIN posterminal pt ON pt.id = pc.posterminal_id -INNER JOIN company co ON co.id = pt.company_id -WHERE pc.posterminal_id = @posterminalId - AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) - AND DATEDIFF_BIG(second, '1970-01-01', c.updated_at) > @cursor +FROM ( + SELECT MAX(q.id) AS syncId, c.id + FROM posterminalsyncqueue q + INNER JOIN category c ON c.id = q.entity_id + INNER JOIN posterminalcategory pc ON pc.category_id = c.id AND pc.posterminal_id = q.posterminal_id + INNER JOIN posterminal pt ON pt.id = pc.posterminal_id + INNER JOIN company co ON co.id = pt.company_id + WHERE q.posterminal_id = @posterminalId + AND q.entity_type = 'category' + AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) + GROUP BY c.id + HAVING MAX(q.id) > @cursor +) t `; export const COUNT_CUSTOMERS = ` SELECT COUNT(*) AS count -FROM entity e -INNER JOIN customergroup cg ON cg.id = e.customergroup_id -INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id -WHERE cg.name <> 'Lieferanten' - AND DATEDIFF_BIG(second, '1970-01-01', e.updated_at) > @cursor +FROM ( + SELECT MAX(q.id) AS syncId, e.id + FROM posterminalsyncqueue q + INNER JOIN entity e ON e.id = q.entity_id + INNER JOIN customergroup cg ON cg.id = e.customergroup_id + INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = cg.company_id + WHERE q.posterminal_id = @posterminalId + AND q.entity_type = 'customer' + AND cg.name <> 'Lieferanten' + GROUP BY e.id + HAVING MAX(q.id) > @cursor +) t `; export const COUNT_CUSTOMER_GROUPS = ` SELECT COUNT(*) AS count -FROM customergroup cg -INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id -WHERE cg.name <> 'Lieferanten' - AND DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) > @cursor +FROM ( + SELECT MAX(q.id) AS syncId, cg.id + FROM posterminalsyncqueue q + INNER JOIN customergroup cg ON cg.id = q.entity_id + INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = cg.company_id + WHERE q.posterminal_id = @posterminalId + AND q.entity_type = 'customergroup' + AND cg.name <> 'Lieferanten' + GROUP BY cg.id + HAVING MAX(q.id) > @cursor +) t `; export const COUNT_COMPOSITE_PRODUCTS = ` -SELECT COUNT(DISTINCT a.id) AS count -FROM article a -INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id -WHERE a.articlelist_id IS NOT NULL - AND EXISTS ( - SELECT 1 - FROM articlelistarticle ala - WHERE ala.articlelist_id = a.articlelist_id - ) - AND EXISTS ( - SELECT 1 - FROM categoryarticle ca - INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id - WHERE ca.article_id = a.id - AND pc.posterminal_id = @posterminalId - ) - AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor +SELECT COUNT(*) AS count +FROM ( + SELECT MAX(q.id) AS syncId, a.id + FROM posterminalsyncqueue q + INNER JOIN article a ON a.id = q.entity_id + INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = a.company_id + WHERE q.posterminal_id = @posterminalId + AND q.entity_type = 'article' + AND a.articlelist_id IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM articlelistarticle ala + WHERE ala.articlelist_id = a.articlelist_id + ) + AND EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id + WHERE ca.article_id = a.id + AND pc.posterminal_id = @posterminalId + ) + GROUP BY a.id + HAVING MAX(q.id) > @cursor +) t `; export const MAX_ORDER_ID = ` @@ -255,16 +289,21 @@ SELECT TOP (@limit) WHERE cp.category_id = c.id ORDER BY cp.sort ASC, cp.id ASC ) AS imgHash, - DATEDIFF_BIG(second, '1970-01-01', c.updated_at) AS lastChanged -FROM category c -INNER JOIN posterminalcategory pc ON pc.category_id = c.id + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'category' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN category c ON c.id = sq.entity_id +INNER JOIN posterminalcategory pc ON pc.category_id = c.id AND pc.posterminal_id = @posterminalId INNER JOIN posterminal pt ON pt.id = pc.posterminal_id INNER JOIN company co ON co.id = pt.company_id LEFT JOIN categorytext ct ON ct.category_id = c.id AND ct.language_id = co.default_language_id -WHERE pc.posterminal_id = @posterminalId - AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) - AND DATEDIFF_BIG(second, '1970-01-01', c.updated_at) > @cursor -ORDER BY DATEDIFF_BIG(second, '1970-01-01', c.updated_at) ASC, c.id ASC +WHERE (co.root_category_id IS NULL OR c.id <> co.root_category_id) +ORDER BY sq.syncId ASC `; export const LIST_POS_PRODUCTS = ` @@ -275,7 +314,7 @@ SELECT TOP (@limit) COALESCE(pr.net, 0) AS netPrice, COALESCE(ptax.rate, ctax.rate, 0) AS taxRate, a.created_at AS createdAt, - (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) AS lastChanged, + sq.syncId AS lastChanged, COALESCE( ( SELECT TOP 1 p.hash @@ -374,7 +413,14 @@ SELECT TOP (@limit) co.id AS companyId, co.root_category_id AS rootCategoryId, co.poscustomergroup_id AS posCustomerGroupId -FROM article a +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'article' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN article a ON a.id = sq.entity_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id INNER JOIN company co ON co.id = a.company_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = co.default_language_id @@ -388,8 +434,7 @@ WHERE EXISTS ( WHERE ca.article_id = a.id AND pc.posterminal_id = @posterminalId ) -AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor -ORDER BY (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) ASC +ORDER BY sq.syncId ASC `; export const LIST_POS_PRODUCT_CATEGORIES = ` @@ -411,13 +456,19 @@ SELECT TOP (@limit) cg.name, CASE WHEN cg.id = co.poscustomergroup_id THEN '1' ELSE '0' END AS standard, '0.000000' AS discountPercent, - DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) AS lastChanged -FROM customergroup cg + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'customergroup' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN customergroup cg ON cg.id = sq.entity_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id INNER JOIN company co ON co.id = pt.company_id WHERE cg.name <> 'Lieferanten' - AND DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) > @cursor -ORDER BY DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) ASC, cg.id ASC +ORDER BY sq.syncId ASC `; export const LIST_POS_CUSTOMERS = ` @@ -469,13 +520,19 @@ SELECT TOP (@limit) WHERE ec.entity_id = e.id AND ec.contacttype IN ('email', 'mail') ORDER BY ec.sort ASC, ec.id ASC ) AS email, - DATEDIFF_BIG(second, '1970-01-01', e.updated_at) AS lastChanged -FROM entity e + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'customer' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN entity e ON e.id = sq.entity_id INNER JOIN customergroup cg ON cg.id = e.customergroup_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id WHERE cg.name <> 'Lieferanten' - AND DATEDIFF_BIG(second, '1970-01-01', e.updated_at) > @cursor -ORDER BY DATEDIFF_BIG(second, '1970-01-01', e.updated_at) ASC, e.id ASC +ORDER BY sq.syncId ASC `; @@ -541,6 +598,34 @@ FROM posterminal pt WHERE pt.company_id = @companyId AND pt.token IS NOT NULL `; +export const RECORD_SYNC_QUEUE = ` +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, @entityType, @entityId +FROM posterminal pt +WHERE pt.company_id = @companyId +`; + +export const SEED_SYNC_QUEUE_FOR_TERMINAL = ` +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT @posterminalId, 'customergroup', cg.id +FROM customergroup cg +WHERE cg.company_id = @companyId +UNION ALL +SELECT @posterminalId, 'customer', e.id +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE cg.company_id = @companyId +UNION ALL +SELECT @posterminalId, 'category', c.id +FROM category c +INNER JOIN posterminalcategory pc ON pc.category_id = c.id +WHERE pc.posterminal_id = @posterminalId +UNION ALL +SELECT @posterminalId, 'article', a.id +FROM article a +WHERE a.company_id = @companyId +`; + export const COUNT_DELETED_ENTITIES = ` SELECT COUNT(*) AS count FROM posterminalobituary diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 2ba11f0..c678318 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -417,6 +417,23 @@ BEGIN END `; +export const INIT_POSTERMINALSYNCQUEUE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminalsyncqueue') +BEGIN + CREATE TABLE posterminalsyncqueue ( + id INT IDENTITY(1,1) PRIMARY KEY, + posterminal_id INT NOT NULL, + entity_type NVARCHAR(50) NOT NULL, + entity_id INT NOT NULL, + CONSTRAINT CK_posterminalsyncqueue_entity_type CHECK (entity_type IN ('article', 'category', 'customer', 'customergroup')), + CONSTRAINT FK_posterminalsyncqueue_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE + ); + + CREATE INDEX IX_posterminalsyncqueue_posterminal_id + ON posterminalsyncqueue (posterminal_id, id); +END +`; + export const INIT_ARTICLEEAN = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean') @@ -1642,6 +1659,7 @@ export const SCHEMA_SCRIPTS = [ INIT_POSTERMINAL, INIT_POSTERMINALCATEGORY, INIT_POSTERMINALOBITUARY, + INIT_POSTERMINALSYNCQUEUE, INIT_PRINTER, INIT_LABELTEMPLATE, INIT_FILE, diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 9608f3c..74b08a5 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -13,6 +13,7 @@ export const { SET_PARENT_SORT, SET_IS_PARENT, TOGGLE_PARENT, TOUCH_UPDATED_AT, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT, + ENQUEUE_SYNC_BY_ARTICLE, ENQUEUE_OPTION_PARENT_ARTICLES_SYNC, ENQUEUE_STUECKLISTE_ARTICLES_SYNC, UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT, UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT, } = queries; diff --git a/src/providers/sqlite3/queries/category.js b/src/providers/sqlite3/queries/category.js index df4cb69..0640b97 100644 --- a/src/providers/sqlite3/queries/category.js +++ b/src/providers/sqlite3/queries/category.js @@ -163,4 +163,41 @@ WHERE id IN ( SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) ) AND parent.parentarticle_id IS NOT NULL ) +`; + +export const ENQUEUE_SUBTREE_CATEGORIES_SYNC = ` +WITH RECURSIVE cats AS ( + SELECT id FROM category WHERE id = @id + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +) +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'category', cats.id +FROM cats +INNER JOIN posterminal pt ON pt.company_id = @companyId +`; + +export const ENQUEUE_SUBTREE_ARTICLES_SYNC = ` +WITH RECURSIVE cats AS ( + SELECT id FROM category WHERE id = @id + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +), +arts AS ( + SELECT ca.article_id AS id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + UNION + SELECT child.id FROM article child + WHERE child.parentarticle_id IN ( + SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + ) + UNION + SELECT parent.parentarticle_id AS id FROM article parent + WHERE parent.id IN ( + SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats) + ) AND parent.parentarticle_id IS NOT NULL +) +INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) +SELECT pt.id, 'article', arts.id +FROM arts +INNER JOIN posterminal pt ON pt.company_id = @companyId `; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/entity.js b/src/providers/sqlite3/queries/entity.js index ca142e5..487308e 100644 --- a/src/providers/sqlite3/queries/entity.js +++ b/src/providers/sqlite3/queries/entity.js @@ -7,8 +7,8 @@ export const { GET_BY_COMPANY_AND_ID, GET_CUSTOMER_BY_ID, GET_GROUP_ID_FOR_SUPPLIER, GET_SUPPLIER_BY_ID, LIST_BY_COMPANY, LIST_BY_COMPANY_AND_GROUP, LIST_CUSTOMERS_BY_COMPANY, LIST_SUPPLIERS_BY_COMPANY, - NUMBER_TAKEN_IN_COMPANY, POS_GROUP_NAME, SUPPLIER_GROUP_NAME, UPDATE, - UPDATE_NAME, + NUMBER_TAKEN_IN_COMPANY, POS_GROUP_NAME, SUPPLIER_GROUP_NAME, + TOUCH_UPDATED_AT, UPDATE, UPDATE_NAME, } = queries; export const DELETE_BY_COMPANY_AND_ID = ` diff --git a/src/providers/sqlite3/queries/posterminal.js b/src/providers/sqlite3/queries/posterminal.js index 1c8cbc2..2f7773f 100644 --- a/src/providers/sqlite3/queries/posterminal.js +++ b/src/providers/sqlite3/queries/posterminal.js @@ -10,7 +10,8 @@ export const { COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID, LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES, LIST_POS_MANDATED_OPTIONS, LIST_POS_PARENT_OPTIONS, - TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES, + TOUCH_LASTSEEN, RECORD_OBITUARY, RECORD_SYNC_QUEUE, SEED_SYNC_QUEUE_FOR_TERMINAL, + COUNT_DELETED_ENTITIES, } = queries; export const SET_ON = ` @@ -50,13 +51,19 @@ SELECT cg.name, CASE WHEN cg.id = co.poscustomergroup_id THEN '1' ELSE '0' END AS standard, '0.000000' AS discountPercent, - CAST(strftime('%s', cg.updated_at) AS INTEGER) AS lastChanged -FROM customergroup cg + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'customergroup' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN customergroup cg ON cg.id = sq.entity_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id INNER JOIN company co ON co.id = pt.company_id WHERE cg.name <> 'Lieferanten' - AND CAST(strftime('%s', cg.updated_at) AS INTEGER) > @cursor -ORDER BY lastChanged ASC, cg.id ASC +ORDER BY sq.syncId ASC LIMIT @limit `; @@ -116,13 +123,19 @@ SELECT ORDER BY ec.sort ASC, ec.id ASC LIMIT 1 ) AS email, - CAST(strftime('%s', e.updated_at) AS INTEGER) AS lastChanged -FROM entity e + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'customer' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN entity e ON e.id = sq.entity_id INNER JOIN customergroup cg ON cg.id = e.customergroup_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id WHERE cg.name <> 'Lieferanten' - AND CAST(strftime('%s', e.updated_at) AS INTEGER) > @cursor -ORDER BY lastChanged ASC, e.id ASC +ORDER BY sq.syncId ASC LIMIT @limit `; @@ -141,16 +154,21 @@ SELECT ORDER BY cp.sort ASC, cp.id ASC LIMIT 1 ) AS imgHash, - CAST(strftime('%s', c.updated_at) AS INTEGER) AS lastChanged -FROM category c -INNER JOIN posterminalcategory pc ON pc.category_id = c.id + sq.syncId AS lastChanged +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'category' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN category c ON c.id = sq.entity_id +INNER JOIN posterminalcategory pc ON pc.category_id = c.id AND pc.posterminal_id = @posterminalId INNER JOIN posterminal pt ON pt.id = pc.posterminal_id INNER JOIN company co ON co.id = pt.company_id LEFT JOIN categorytext ct ON ct.category_id = c.id AND ct.language_id = co.default_language_id -WHERE pc.posterminal_id = @posterminalId - AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) - AND CAST(strftime('%s', c.updated_at) AS INTEGER) > @cursor -ORDER BY lastChanged ASC, c.id ASC +WHERE (co.root_category_id IS NULL OR c.id <> co.root_category_id) +ORDER BY sq.syncId ASC LIMIT @limit `; @@ -162,7 +180,7 @@ SELECT COALESCE(pr.net, 0) AS netPrice, COALESCE(ptax.rate, ctax.rate, 0) AS taxRate, a.created_at AS createdAt, - (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) AS lastChanged, + sq.syncId AS lastChanged, COALESCE( ( SELECT p.hash @@ -265,7 +283,14 @@ SELECT co.id AS companyId, co.root_category_id AS rootCategoryId, co.poscustomergroup_id AS posCustomerGroupId -FROM article a +FROM ( + SELECT entity_id, MAX(id) AS syncId + FROM posterminalsyncqueue + WHERE posterminal_id = @posterminalId AND entity_type = 'article' + GROUP BY entity_id + HAVING MAX(id) > @cursor +) sq +INNER JOIN article a ON a.id = sq.entity_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id INNER JOIN company co ON co.id = a.company_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = co.default_language_id @@ -279,8 +304,7 @@ WHERE EXISTS ( WHERE ca.article_id = a.id AND pc.posterminal_id = @posterminalId ) -AND (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) > @cursor -ORDER BY (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) ASC +ORDER BY sq.syncId ASC LIMIT @limit `; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index d501411..8e4af56 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -660,6 +660,18 @@ CREATE INDEX IF NOT EXISTS IX_posterminalobituary_posterminal_id ON posterminalobituary (posterminal_id, id); `; +export const INIT_POSTERMINALSYNCQUEUE = ` +CREATE TABLE IF NOT EXISTS posterminalsyncqueue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + posterminal_id INTEGER NOT NULL, + entity_type TEXT NOT NULL CHECK (entity_type IN ('article', 'category', 'customer', 'customergroup')), + entity_id INTEGER NOT NULL, + FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_posterminalsyncqueue_posterminal_id + ON posterminalsyncqueue (posterminal_id, id); +`; + export const INIT_PRINTER = ` CREATE TABLE IF NOT EXISTS printer ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1206,6 +1218,7 @@ export const SCHEMA_SCRIPTS = [ INIT_POSTERMINAL, INIT_POSTERMINALCATEGORY, INIT_POSTERMINALOBITUARY, + INIT_POSTERMINALSYNCQUEUE, INIT_PRINTER, INIT_LABELTEMPLATE, INIT_FILE, diff --git a/src/store/article/addChild.js b/src/store/article/addChild.js index 0d8b585..1741cc8 100644 --- a/src/store/article/addChild.js +++ b/src/store/article/addChild.js @@ -273,6 +273,8 @@ export default function addChild(provider) { // Touch parent article updated_at so POS and other consumers refresh parent options await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: childId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: parentId }); return load(googleAuthId, childSku, runner); }; diff --git a/src/store/article/addImage.js b/src/store/article/addImage.js index 44cf632..9e94e83 100644 --- a/src/store/article/addImage.js +++ b/src/store/article/addImage.js @@ -8,8 +8,14 @@ import { } from '../../providers/sqlite3/queries/articlepic.js'; import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js'; import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.js'; -import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js'; -import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import processAvifImage from '../../util/processAvifImage.js'; import newFullHash from '../../util/newFullHash.js'; import resolveArticleBySku from './resolveArticleBySku.js'; @@ -24,6 +30,7 @@ export default function addImage(provider) { const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC; const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC; const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Adds an image to an article by its SKU, processing full, thumb, and 32px-edge small-thumb AVIF variants. @@ -107,6 +114,7 @@ export default function addImage(provider) { } await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); return { picId, diff --git a/src/store/article/attachChild.js b/src/store/article/attachChild.js index dd359e0..47b477e 100644 --- a/src/store/article/attachChild.js +++ b/src/store/article/attachChild.js @@ -173,6 +173,8 @@ export default function attachChild(provider) { }); } } + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: prevParentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: prevParentId }); } // Assign parentsort if not already child of this parent @@ -305,6 +307,11 @@ export default function attachChild(provider) { }); } + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: child.id }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: child.id }); + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: parentId }); + const updated = await load(googleAuthId, childSku, runner); const children = await list(googleAuthId, parentSku, runner); diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index 4090588..2758199 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -1,10 +1,12 @@ import { CREATE as MSSQL_CREATE_ARTICLE, CHECK_HAS_WAREHOUSE as MSSQL_CHECK_HAS_WAREHOUSE, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, } from '../../providers/mssql/queries/article.js'; import { CREATE as SQLITE_CREATE_ARTICLE, CHECK_HAS_WAREHOUSE as SQLITE_CHECK_HAS_WAREHOUSE, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, } from '../../providers/sqlite3/queries/article.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'; @@ -30,6 +32,7 @@ export default function createUnderParent(provider) { const checkHasWarehouseSql = isSqlite ? SQLITE_CHECK_HAS_WAREHOUSE : MSSQL_CHECK_HAS_WAREHOUSE; const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE; const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Creates a new article under a given parent category, allocating a SKU and @@ -118,6 +121,8 @@ export default function createUnderParent(provider) { await initArticleWarehouseSummary(runner, { companyId, articleId: id }); } + await runner.query(enqueueSyncSql, { articleId: id }); + return { id, parentId, diff --git a/src/store/article/delete.js b/src/store/article/delete.js index 5001f5e..be40289 100644 --- a/src/store/article/delete.js +++ b/src/store/article/delete.js @@ -51,6 +51,7 @@ export default function del(provider) { // Touch any parent articles that used this article as an option before deleting it await runner.query(articleQueries.TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT, { articleId: id }); + await runner.query(articleQueries.ENQUEUE_OPTION_PARENT_ARTICLES_SYNC, { articleId: id }); const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id }); if (!deleted.rowsAffected) { diff --git a/src/store/article/deleteImage.js b/src/store/article/deleteImage.js index d35a418..348b26a 100644 --- a/src/store/article/deleteImage.js +++ b/src/store/article/deleteImage.js @@ -2,16 +2,24 @@ import sharp from 'sharp'; import { encodeRGBA } from 'skelehash-wasm'; import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js'; import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.js'; -import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js'; -import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import { recordImageDeleteHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; export default function deleteImage(provider) { const resolve = resolveArticleBySku(provider); - const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY; - const touchArticleSql = provider?.dialect === 'sqlite3' ? SQLITE_TOUCH : MSSQL_TOUCH; + const isSqlite = provider?.dialect === 'sqlite3'; + const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Deletes an image from an article by its SKU and full image hash. @@ -86,6 +94,7 @@ export default function deleteImage(provider) { }); await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); }; if (tx) { diff --git a/src/store/article/orderImages.js b/src/store/article/orderImages.js index 725cd01..a6203f7 100644 --- a/src/store/article/orderImages.js +++ b/src/store/article/orderImages.js @@ -1,7 +1,13 @@ import * as mssqlPic from '../../providers/mssql/queries/articlepic.js'; import * as sqlitePic from '../../providers/sqlite3/queries/articlepic.js'; -import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js'; -import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import { recordImageOrderHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; @@ -11,6 +17,7 @@ export default function orderImages(provider) { const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqlitePic : mssqlPic; const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Reorders the images of an article by its SKU. @@ -73,6 +80,7 @@ export default function orderImages(provider) { fullHashes, }); await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); } return { fullHashes }; diff --git a/src/store/article/removeChild.js b/src/store/article/removeChild.js index fcc2e7f..ab855e2 100644 --- a/src/store/article/removeChild.js +++ b/src/store/article/removeChild.js @@ -88,6 +88,7 @@ export default function removeChild(provider) { // Touch parent article updated_at so POS and other consumers refresh parent options await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: parentId }); return { success: true, @@ -134,8 +135,11 @@ export default function removeChild(provider) { parentSku, }); - // Touch parent article updated_at so POS and other consumers refresh parent options + // Touch child and parent article updated_at so POS and other consumers refresh parent options + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: childId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: childId }); await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: parentId }); return { success: true, diff --git a/src/store/article/setBrandIdBySku.js b/src/store/article/setBrandIdBySku.js index 53bc46a..a18b0d3 100644 --- a/src/store/article/setBrandIdBySku.js +++ b/src/store/article/setBrandIdBySku.js @@ -1,10 +1,12 @@ import { GET_BRAND_BY_COMPANY_AND_ID as MSSQL_GET_BRAND, UPDATE_BRAND as MSSQL_UPDATE_BRAND, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, } from '../../providers/mssql/queries/article.js'; import { GET_BRAND_BY_COMPANY_AND_ID as SQLITE_GET_BRAND, UPDATE_BRAND as SQLITE_UPDATE_BRAND, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, } from '../../providers/sqlite3/queries/article.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; @@ -18,6 +20,7 @@ export default function setBrandIdBySku(provider) { const getBrandSql = isSqlite ? SQLITE_GET_BRAND : MSSQL_GET_BRAND; const updateBrandSql = isSqlite ? SQLITE_UPDATE_BRAND : MSSQL_UPDATE_BRAND; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Sets (or clears) the brand of an article by its SKU. @@ -79,6 +82,8 @@ export default function setBrandIdBySku(provider) { throw new Error('Article not found'); } + await runner.query(enqueueSyncSql, { articleId }); + // null = no brand; treat as distinct sentinel so first assignment is logged await recordBrandChangeHistory(runner, { articleId, diff --git a/src/store/article/setCategoryIdsBySku.js b/src/store/article/setCategoryIdsBySku.js index 8d6cf62..66369e1 100644 --- a/src/store/article/setCategoryIdsBySku.js +++ b/src/store/article/setCategoryIdsBySku.js @@ -5,6 +5,7 @@ import { INSERT_POSTERMINAL_OBITUARY as MSSQL_INSERT_OBITUARY, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE as MSSQL_LIST_TERMINALS, TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, } from '../../providers/mssql/queries/article.js'; import { ADD_CATEGORY as SQLITE_ADD_CATEGORY, @@ -13,6 +14,7 @@ import { INSERT_POSTERMINAL_OBITUARY as SQLITE_INSERT_OBITUARY, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE as SQLITE_LIST_TERMINALS, TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, } from '../../providers/sqlite3/queries/article.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; import queryBySku from './queryBySku.js'; @@ -32,6 +34,7 @@ export default function setCategoryIdsBySku(provider) { const listTerminalsSql = isSqlite ? SQLITE_LIST_TERMINALS : MSSQL_LIST_TERMINALS; const insertObituarySql = isSqlite ? SQLITE_INSERT_OBITUARY : MSSQL_INSERT_OBITUARY; const clearObituarySql = isSqlite ? SQLITE_CLEAR_OBITUARY : MSSQL_CLEAR_OBITUARY; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Replaces the category membership of an article by its SKU. @@ -150,6 +153,7 @@ export default function setCategoryIdsBySku(provider) { // Touch article updated_at so POS and other consumers pick up category assignment changes await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); const article = await load(articleId, languageId, runner); if (!article) { diff --git a/src/store/article/setEansBySku.js b/src/store/article/setEansBySku.js index 0ce177d..d055052 100644 --- a/src/store/article/setEansBySku.js +++ b/src/store/article/setEansBySku.js @@ -3,12 +3,14 @@ import { DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS, EAN_EXISTS_FOR_OTHER_ARTICLE as MSSQL_CHECK_CONFLICT, TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, } from '../../providers/mssql/queries/article.js'; import { ADD_EAN as SQLITE_ADD_EAN, DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS, EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT, TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, } from '../../providers/sqlite3/queries/article.js'; import { isUniqueConstraintError } from '../../providers/provider.js'; import queryBySku from './queryBySku.js'; @@ -25,6 +27,7 @@ export default function setEansBySku(provider) { const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS; const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT; const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Replaces the EAN list of an article by its SKU. @@ -103,6 +106,7 @@ export default function setEansBySku(provider) { // Touch article updated_at so POS and other consumers pick up the barcode change await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); const article = await load(articleId, languageId, runner); if (!article) { diff --git a/src/store/article/setParentBySku.js b/src/store/article/setParentBySku.js index bcf3dac..4b96302 100644 --- a/src/store/article/setParentBySku.js +++ b/src/store/article/setParentBySku.js @@ -56,6 +56,9 @@ export default function setParentBySku(provider) { parentArticleId: null, }); + await runner.query(queries.TOUCH_UPDATED_AT, { articleId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId }); + if (prevParentId) { const prevParentTextRes = await runner.query( 'SELECT sku FROM article WHERE id = @prevParentId', @@ -74,6 +77,9 @@ export default function setParentBySku(provider) { companyuserId, text: `Variante "${childName}" (${sku}) getrennt`, }); + + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: prevParentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: prevParentId }); } const unlinked = await load(articleId, languageId, runner); @@ -141,6 +147,11 @@ export default function setParentBySku(provider) { parentSku, }); + await runner.query(queries.TOUCH_UPDATED_AT, { articleId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId }); + await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId }); + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId: parentId }); + const article = await load(articleId, languageId, runner); if (!article) { throw new Error('Article not found'); diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js index eab1661..efe3bb3 100644 --- a/src/store/article/setPricesBySku.js +++ b/src/store/article/setPricesBySku.js @@ -9,10 +9,14 @@ import { import { TOUCH_UPDATED_AT as MSSQL_TOUCH, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT as MSSQL_TOUCH_OPTION_PARENTS, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, + ENQUEUE_OPTION_PARENT_ARTICLES_SYNC as MSSQL_ENQUEUE_OPTION_PARENTS, } from '../../providers/mssql/queries/article.js'; import { TOUCH_UPDATED_AT as SQLITE_TOUCH, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT as SQLITE_TOUCH_OPTION_PARENTS, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, + ENQUEUE_OPTION_PARENT_ARTICLES_SYNC as SQLITE_ENQUEUE_OPTION_PARENTS, } from '../../providers/sqlite3/queries/article.js'; import taxEnsure from '../tax/ensure.js'; import queryBySku from './queryBySku.js'; @@ -75,6 +79,8 @@ export default function setPricesBySku(provider) { const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE; const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; const touchOptionParentsSql = isSqlite ? SQLITE_TOUCH_OPTION_PARENTS : MSSQL_TOUCH_OPTION_PARENTS; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; + const enqueueOptionParentsSql = isSqlite ? SQLITE_ENQUEUE_OPTION_PARENTS : MSSQL_ENQUEUE_OPTION_PARENTS; /** * Replaces the price list of an article by its SKU. @@ -153,9 +159,11 @@ export default function setPricesBySku(provider) { // Touch article updated_at so POS and other consumers pick up the price change await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); // Touch any parent articles using this article as an option so their deposit price updates await runner.query(touchOptionParentsSql, { articleId }); + await runner.query(enqueueOptionParentsSql, { articleId }); const article = await load(articleId, languageId, runner); if (!article) { diff --git a/src/store/article/setTrackStockBySku.js b/src/store/article/setTrackStockBySku.js index f07d970..079943a 100644 --- a/src/store/article/setTrackStockBySku.js +++ b/src/store/article/setTrackStockBySku.js @@ -2,11 +2,13 @@ import { UPDATE_TRACK_STOCK as MSSQL_UPDATE_TRACK_STOCK, CHECK_HAS_WAREHOUSE as MSSQL_CHECK_HAS_WAREHOUSE, CHECK_ARTICLE_WAREHOUSE_STOCKS as MSSQL_CHECK_ARTICLE_WAREHOUSE_STOCKS, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, } from '../../providers/mssql/queries/article.js'; import { UPDATE_TRACK_STOCK as SQLITE_UPDATE_TRACK_STOCK, CHECK_HAS_WAREHOUSE as SQLITE_CHECK_HAS_WAREHOUSE, CHECK_ARTICLE_WAREHOUSE_STOCKS as SQLITE_CHECK_ARTICLE_WAREHOUSE_STOCKS, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, } from '../../providers/sqlite3/queries/article.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; @@ -26,6 +28,7 @@ export default function setTrackStockBySku(provider) { const sql = isSqlite ? SQLITE_UPDATE_TRACK_STOCK : MSSQL_UPDATE_TRACK_STOCK; const checkHasWarehouseSql = isSqlite ? SQLITE_CHECK_HAS_WAREHOUSE : MSSQL_CHECK_HAS_WAREHOUSE; const checkStocksSql = isSqlite ? SQLITE_CHECK_ARTICLE_WAREHOUSE_STOCKS : MSSQL_CHECK_ARTICLE_WAREHOUSE_STOCKS; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Sets the track_stock flag of an article by its SKU. @@ -96,6 +99,8 @@ export default function setTrackStockBySku(provider) { throw new Error('Article not found'); } + await runner.query(enqueueSyncSql, { articleId }); + // Write history if track_stock changed if (oldTrack !== newTrack) { if (!newTrack) { diff --git a/src/store/article/setWeightBySku.js b/src/store/article/setWeightBySku.js index 2127c60..24c30da 100644 --- a/src/store/article/setWeightBySku.js +++ b/src/store/article/setWeightBySku.js @@ -1,5 +1,11 @@ -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 { + UPDATE_WEIGHT as MSSQL_UPDATE_WEIGHT, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + UPDATE_WEIGHT as SQLITE_UPDATE_WEIGHT, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import { recordWeightChangeHistory } from '../articlehistory/record.js'; @@ -8,7 +14,9 @@ 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 isSqlite = provider?.dialect === 'sqlite3'; + const sql = isSqlite ? SQLITE_UPDATE_WEIGHT : MSSQL_UPDATE_WEIGHT; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Sets the weight of an article by its SKU. @@ -48,6 +56,8 @@ export default function setWeightBySku(provider) { throw new Error('Article not found'); } + await runner.query(enqueueSyncSql, { articleId }); + // Write history if weight changed await recordWeightChangeHistory(runner, { articleId, diff --git a/src/store/article/toggleParent.js b/src/store/article/toggleParent.js index 6124b40..2dd1f84 100644 --- a/src/store/article/toggleParent.js +++ b/src/store/article/toggleParent.js @@ -171,6 +171,8 @@ export default function toggleParent(provider) { }); } + await runner.query(queries.ENQUEUE_SYNC_BY_ARTICLE, { articleId }); + return load(googleAuthId, sku, runner); }; diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index 36d41eb..8f63032 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -234,9 +234,11 @@ export default function updateBySku(provider) { // Touch article updated_at so all consumers pick up changes await runner.query(articleQueries.TOUCH_UPDATED_AT, { articleId }); + await runner.query(articleQueries.ENQUEUE_SYNC_BY_ARTICLE, { articleId }); // Touch any parent articles that use this article as an option so their deposit name/details update on POS await runner.query(articleQueries.TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT, { articleId }); + await runner.query(articleQueries.ENQUEUE_OPTION_PARENT_ARTICLES_SYNC, { articleId }); const article = await load(articleId, languageId, runner); if (!article) { diff --git a/src/store/articleattributevalue/setByArticleSku.js b/src/store/articleattributevalue/setByArticleSku.js index 6280f89..1962830 100644 --- a/src/store/articleattributevalue/setByArticleSku.js +++ b/src/store/articleattributevalue/setByArticleSku.js @@ -8,6 +8,14 @@ import { INSERT_ROW as SQLITE_INSERT, GET_VALUE_DETAIL as SQLITE_GET_DETAIL, } from '../../providers/sqlite3/queries/articleattributevalue.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import { recordAttributeChangeHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; @@ -18,6 +26,8 @@ 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 touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Replaces all attribute values for an article by its SKU. @@ -187,6 +197,13 @@ export default function setByArticleSku(provider) { newAttrs: validated, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + if (article?.parentarticle_id != null) { + await runner.query(touchArticleSql, { articleId: article.parentarticle_id }); + await runner.query(enqueueSyncSql, { articleId: article.parentarticle_id }); + } + return inserted; }; diff --git a/src/store/articlelist/setItemsByArticleSku.js b/src/store/articlelist/setItemsByArticleSku.js index 1cb4bd5..30f4637 100644 --- a/src/store/articlelist/setItemsByArticleSku.js +++ b/src/store/articlelist/setItemsByArticleSku.js @@ -1,5 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlelist.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/articlelist.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import getItemsByArticleSku from './getItemsByArticleSku.js'; import { @@ -46,6 +54,8 @@ export default function setItemsByArticleSku(provider) { const getItems = getItemsByArticleSku(provider); const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Sets or clears the Stückliste (bundle items) for an article. @@ -108,6 +118,8 @@ export default function setItemsByArticleSku(provider) { type: 'articlelist_cleared', }); } + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); } return []; } @@ -229,6 +241,9 @@ export default function setItemsByArticleSku(provider) { items: result, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + return result; }; diff --git a/src/store/articleproperty/delete.js b/src/store/articleproperty/delete.js index 76ea7cc..3eca457 100644 --- a/src/store/articleproperty/delete.js +++ b/src/store/articleproperty/delete.js @@ -4,6 +4,14 @@ import { import { DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE, } from '../../providers/sqlite3/queries/articleproperty.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import { recordPropertyDeleteHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; @@ -12,6 +20,8 @@ export default function deleteArticleProperty(provider) { const resolve = resolveArticleBySku(provider); const isSqlite = provider?.dialect === 'sqlite3'; const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Deletes a property from an article by SKU and propertyId. @@ -45,6 +55,8 @@ export default function deleteArticleProperty(provider) { name: prop.name, value: prop.value, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); } }; } diff --git a/src/store/articleproperty/setByArticleSku.js b/src/store/articleproperty/setByArticleSku.js index 983e717..559a631 100644 --- a/src/store/articleproperty/setByArticleSku.js +++ b/src/store/articleproperty/setByArticleSku.js @@ -12,6 +12,14 @@ import { import { GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP, } from '../../providers/sqlite3/queries/propertyname.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import { recordPropertyChangeHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; @@ -22,6 +30,8 @@ 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 touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Replaces all properties for an article by its SKU. @@ -124,6 +134,9 @@ export default function setByArticleSku(provider) { newProps: validated, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + return inserted; }; diff --git a/src/store/articlesupplier/create.js b/src/store/articlesupplier/create.js index f41e929..951396a 100644 --- a/src/store/articlesupplier/create.js +++ b/src/store/articlesupplier/create.js @@ -1,5 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import taxEnsure from '../tax/ensure.js'; import { @@ -17,6 +25,8 @@ export default function create(provider) { const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Assigns a supplier to an article, optionally creating a purchase price. @@ -161,6 +171,9 @@ export default function create(provider) { pu: actualPu, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + return shapeArticleSupplier(supplierRow); }; diff --git a/src/store/articlesupplier/delete.js b/src/store/articlesupplier/delete.js index 84cd076..ca87395 100644 --- a/src/store/articlesupplier/delete.js +++ b/src/store/articlesupplier/delete.js @@ -1,5 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import { recordSupplierDeleteHistory } from '../articlehistory/record.js'; import makeRunner from '../runner.js'; @@ -8,6 +16,8 @@ export default function del(provider) { const resolve = resolveArticleBySku(provider); const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Deletes a supplier assignment and cleans up its associated purchase price. @@ -59,6 +69,9 @@ export default function del(provider) { supplierName, supplierSku: existing.sku, }); + + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); }; if (tx) { diff --git a/src/store/articlesupplier/order.js b/src/store/articlesupplier/order.js index 9b02b2f..d857f83 100644 --- a/src/store/articlesupplier/order.js +++ b/src/store/articlesupplier/order.js @@ -1,5 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import makeRunner from '../runner.js'; @@ -7,6 +15,8 @@ export default function order(provider) { const resolve = resolveArticleBySku(provider); const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Reorders the supplier assignments of an article by its SKU. @@ -65,6 +75,9 @@ export default function order(provider) { } } + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + return { ids }; }; diff --git a/src/store/articlesupplier/update.js b/src/store/articlesupplier/update.js index 1a0d00e..8acc848 100644 --- a/src/store/articlesupplier/update.js +++ b/src/store/articlesupplier/update.js @@ -1,5 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import taxEnsure from '../tax/ensure.js'; import { @@ -16,6 +24,8 @@ export default function update(provider) { const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; + const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Updates an existing supplier assignment and its associated purchase price. @@ -218,6 +228,9 @@ export default function update(provider) { supplierName, }); + await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, { id, articleId, diff --git a/src/store/articlewarehousesummary/syncSummary.js b/src/store/articlewarehousesummary/syncSummary.js index 7c8a169..f871183 100644 --- a/src/store/articlewarehousesummary/syncSummary.js +++ b/src/store/articlewarehousesummary/syncSummary.js @@ -1,10 +1,14 @@ import { TOUCH_UPDATED_AT as MSSQL_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, + ENQUEUE_STUECKLISTE_ARTICLES_SYNC as MSSQL_ENQUEUE_STUECKLISTE_SYNC, } from '../../providers/mssql/queries/article.js'; import { TOUCH_UPDATED_AT as SQLITE_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, + ENQUEUE_STUECKLISTE_ARTICLES_SYNC as SQLITE_ENQUEUE_STUECKLISTE_SYNC, } from '../../providers/sqlite3/queries/article.js'; /** @@ -154,8 +158,12 @@ export async function syncArticleWarehouseSummaryForLedger(runner, { companyId, const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; + const enqueueStuecklisteSyncSql = isSqlite ? SQLITE_ENQUEUE_STUECKLISTE_SYNC : MSSQL_ENQUEUE_STUECKLISTE_SYNC; await runner.query(touchSql, { articleId }); await runner.query(touchStuecklisteSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); + await runner.query(enqueueStuecklisteSyncSql, { articleId }); } } diff --git a/src/store/attributevalue/update.js b/src/store/attributevalue/update.js index e4d3d38..64644f7 100644 --- a/src/store/attributevalue/update.js +++ b/src/store/attributevalue/update.js @@ -62,6 +62,51 @@ export default function update(provider) { throw err; } + if (value !== existing.rows[0].value || sort !== existing.rows[0].sort) { + const touchSql = isSqlite + ? `UPDATE article SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + WHERE company_id = @companyId + AND ( + id IN (SELECT article_id FROM articleattributevalue WHERE attributevalue_id = @valId) + OR id IN ( + SELECT a.parentarticle_id + FROM article a + INNER JOIN articleattributevalue aav ON aav.article_id = a.id + WHERE aav.attributevalue_id = @valId AND a.parentarticle_id IS NOT NULL + ) + )` + : `UPDATE article SET updated_at = SYSUTCDATETIME() + WHERE company_id = @companyId + AND ( + id IN (SELECT article_id FROM articleattributevalue WHERE attributevalue_id = @valId) + OR id IN ( + SELECT a.parentarticle_id + FROM article a + INNER JOIN articleattributevalue aav ON aav.article_id = a.id + WHERE aav.attributevalue_id = @valId AND a.parentarticle_id IS NOT NULL + ) + )`; + await runner.query(touchSql, { valId, companyId }); + + const enqueueSql = ` + INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) + SELECT pt.id, 'article', a.id + FROM article a + INNER JOIN posterminal pt ON pt.company_id = a.company_id + WHERE a.company_id = @companyId + AND ( + a.id IN (SELECT article_id FROM articleattributevalue WHERE attributevalue_id = @valId) + OR a.id IN ( + SELECT child.parentarticle_id + FROM article child + INNER JOIN articleattributevalue aav ON aav.article_id = child.id + WHERE aav.attributevalue_id = @valId AND child.parentarticle_id IS NOT NULL + ) + ) + `; + await runner.query(enqueueSql, { valId, companyId }); + } + return { id: valId, attributeId: existing.rows[0].attributeId, diff --git a/src/store/brand/update.js b/src/store/brand/update.js index d98f100..eeb3d5c 100644 --- a/src/store/brand/update.js +++ b/src/store/brand/update.js @@ -78,6 +78,15 @@ export default function update(provider) { : 'UPDATE article SET updated_at = SYSUTCDATETIME() WHERE brand_id = @brandId AND company_id = @companyId'; const touched = await runner.query(touchSql, { brandId: numericId, companyId }); touchedArticleCount = touched.rowsAffected ?? 0; + + const enqueueSql = ` + INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) + SELECT pt.id, 'article', a.id + FROM article a + INNER JOIN posterminal pt ON pt.company_id = a.company_id + WHERE a.brand_id = @brandId AND a.company_id = @companyId + `; + await runner.query(enqueueSql, { brandId: numericId, companyId }); } return { diff --git a/src/store/category/addImage.js b/src/store/category/addImage.js index f7bad74..792a783 100644 --- a/src/store/category/addImage.js +++ b/src/store/category/addImage.js @@ -11,10 +11,12 @@ import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic import processAvifImage from '../../util/processAvifImage.js'; import newFullHash from '../../util/newFullHash.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function addImage(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; @@ -42,7 +44,7 @@ export default function addImage(provider) { const fullHash = newFullHash(processed.full.buffer); const runAdd = async (runner) => { - await access(googleAuthId, categoryId, runner); + const { companyId } = await access(googleAuthId, categoryId, runner); const sortResult = await runner.query(getSortSql, { categoryId }); const sort = sortResult.rows[0].sort; @@ -95,6 +97,7 @@ export default function addImage(provider) { } await runner.query(touchSql, { categoryId }); + await recordSync({ companyId, entityType: 'category', entityId: categoryId }, runner); return { id: categoryId, diff --git a/src/store/category/createUnderParent.js b/src/store/category/createUnderParent.js index b6a7963..0dd392b 100644 --- a/src/store/category/createUnderParent.js +++ b/src/store/category/createUnderParent.js @@ -9,11 +9,13 @@ import { import { CREATE as MSSQL_CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js'; import { CREATE as SQLITE_CREATE_CATEGORYTEXT } from '../../providers/sqlite3/queries/categorytext.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import slugify from '../slugify.js'; import makeRunner from '../runner.js'; export default function createUnderParent(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite @@ -40,7 +42,7 @@ export default function createUnderParent(provider) { */ return async ({ googleAuthId, parentId, name }, tx) => { const runCreate = async (runner) => { - const { languageId } = await access(googleAuthId, parentId, runner); + const { languageId, companyId } = await access(googleAuthId, parentId, runner); const sortResult = await runner.query(getSortSql, { parentId }); const sort = sortResult.rows[0].sort; @@ -60,6 +62,8 @@ export default function createUnderParent(provider) { slug, }); + await recordSync({ companyId, entityType: 'category', entityId: id }, runner); + return { id, parentId, diff --git a/src/store/category/deleteImage.js b/src/store/category/deleteImage.js index ff9fadf..e1c091a 100644 --- a/src/store/category/deleteImage.js +++ b/src/store/category/deleteImage.js @@ -1,10 +1,12 @@ import { DELETE_BY_CATEGORY as MSSQL_DELETE } from '../../providers/mssql/queries/categorypic.js'; import { DELETE_BY_CATEGORY as SQLITE_DELETE } from '../../providers/sqlite3/queries/categorypic.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function deleteImage(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; const isSqlite = provider?.dialect === 'sqlite3'; const touchSql = isSqlite @@ -26,7 +28,7 @@ export default function deleteImage(provider) { */ return async (googleAuthId, categoryId, tx) => { const runner = makeRunner(provider, tx); - await access(googleAuthId, categoryId, runner); + const { companyId } = await access(googleAuthId, categoryId, runner); const result = await runner.query(sql, { categoryId }); @@ -35,6 +37,7 @@ export default function deleteImage(provider) { } await runner.query(touchSql, { categoryId }); + await recordSync({ companyId, entityType: 'category', entityId: categoryId }, runner); return { id: categoryId, diff --git a/src/store/category/ensureByPath.js b/src/store/category/ensureByPath.js index ad1c3b9..00d6dea 100644 --- a/src/store/category/ensureByPath.js +++ b/src/store/category/ensureByPath.js @@ -11,6 +11,7 @@ import { import { CREATE as MSSQL_CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js'; import { CREATE as SQLITE_CREATE_CATEGORYTEXT } from '../../providers/sqlite3/queries/categorytext.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import slugify from '../slugify.js'; import makeRunner from '../runner.js'; @@ -25,6 +26,7 @@ export function splitPath(path) { export default function ensureByPath(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite @@ -61,7 +63,7 @@ export default function ensureByPath(provider) { let currentId = parentId; - const createCategory = async (runner, parentId, name, languageId) => { + const createCategory = async (runner, parentId, name, languageId, companyId) => { const sortResult = await runner.query(getSortSql, { parentId }); const sort = sortResult.rows[0].sort; const slug = slugify(name); @@ -79,11 +81,13 @@ export default function ensureByPath(provider) { slug, }); + await recordSync({ companyId, entityType: 'category', entityId: id }, runner); + return id; }; for (const name of names) { - const { languageId } = await access(googleAuthId, currentId, tx); + const { languageId, companyId } = await access(googleAuthId, currentId, tx); const existing = await makeRunner(provider, tx).query(getByNameSql, { parentId: currentId, @@ -98,17 +102,17 @@ export default function ensureByPath(provider) { if (tx) { // Run within the caller's transaction (e.g. article import row). - currentId = await createCategory(makeRunner(provider, tx), currentId, name, languageId); + currentId = await createCategory(makeRunner(provider, tx), currentId, name, languageId, companyId); continue; } // Standalone use: own transaction per created category. if (typeof provider?.transaction === 'function') { currentId = await provider.transaction((runner) => - createCategory(runner, currentId, name, languageId), + createCategory(runner, currentId, name, languageId, companyId), ); } else { - currentId = await createCategory(makeRunner(provider), currentId, name, languageId); + currentId = await createCategory(makeRunner(provider), currentId, name, languageId, companyId); } } diff --git a/src/store/category/move.js b/src/store/category/move.js index 016b9ca..62a55cf 100644 --- a/src/store/category/move.js +++ b/src/store/category/move.js @@ -4,6 +4,8 @@ import { MOVE as MSSQL_MOVE, TOUCH_SUBTREE_CATEGORIES as MSSQL_TOUCH_SUBTREE_CATEGORIES, TOUCH_SUBTREE_ARTICLES as MSSQL_TOUCH_SUBTREE_ARTICLES, + ENQUEUE_SUBTREE_CATEGORIES_SYNC as MSSQL_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as MSSQL_ENQUEUE_SUBTREE_ARTICLES, } from '../../providers/mssql/queries/category.js'; import { GET_NEXT_SIBLING_SORT as SQLITE_GET_NEXT_SIBLING_SORT, @@ -11,6 +13,8 @@ import { MOVE as SQLITE_MOVE, TOUCH_SUBTREE_CATEGORIES as SQLITE_TOUCH_SUBTREE_CATEGORIES, TOUCH_SUBTREE_ARTICLES as SQLITE_TOUCH_SUBTREE_ARTICLES, + ENQUEUE_SUBTREE_CATEGORIES_SYNC as SQLITE_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as SQLITE_ENQUEUE_SUBTREE_ARTICLES, } from '../../providers/sqlite3/queries/category.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; import makeRunner from '../runner.js'; @@ -30,6 +34,12 @@ export default function move(provider) { const touchArticlesSql = isSqlite ? SQLITE_TOUCH_SUBTREE_ARTICLES : MSSQL_TOUCH_SUBTREE_ARTICLES; + const enqueueCategoriesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_CATEGORIES + : MSSQL_ENQUEUE_SUBTREE_CATEGORIES; + const enqueueArticlesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_ARTICLES + : MSSQL_ENQUEUE_SUBTREE_ARTICLES; /** * Moves a category to a new parent, preventing cycles and cross-company moves. @@ -82,6 +92,8 @@ export default function move(provider) { await runner.query(touchCategoriesSql, { id }); await runner.query(touchArticlesSql, { id }); + await runner.query(enqueueCategoriesSql, { id, companyId: source.companyId }); + await runner.query(enqueueArticlesSql, { id, companyId: source.companyId }); return { id, parentId, sort }; }; diff --git a/src/store/category/order.js b/src/store/category/order.js index 68d5c6e..de25be1 100644 --- a/src/store/category/order.js +++ b/src/store/category/order.js @@ -7,10 +7,12 @@ import { UPDATE_SORT as SQLITE_UPDATE_SORT, } from '../../providers/sqlite3/queries/category.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function order(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const listChildIdsSql = isSqlite ? SQLITE_LIST_CHILD_IDS @@ -39,7 +41,7 @@ export default function order(provider) { const runOrder = async (runner) => { // Auth + ensures parentId is a real category of the user's company. - await access(googleAuthId, parentId, runner); + const { companyId } = await access(googleAuthId, parentId, runner); const children = await runner.query(listChildIdsSql, { parentId }); @@ -63,6 +65,7 @@ export default function order(provider) { parentId, sort, }); + await recordSync({ companyId, entityType: 'category', entityId: categoryIds[sort] }, runner); } return { categoryIds }; diff --git a/src/store/category/rename.js b/src/store/category/rename.js index 49830f3..c4b7a12 100644 --- a/src/store/category/rename.js +++ b/src/store/category/rename.js @@ -1,11 +1,13 @@ import { UPDATE_BY_CATEGORY_AND_LANGUAGE as MSSQL_UPDATE } from '../../providers/mssql/queries/categorytext.js'; import { UPDATE_BY_CATEGORY_AND_LANGUAGE as SQLITE_UPDATE } from '../../providers/sqlite3/queries/categorytext.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import slugify from '../slugify.js'; import makeRunner from '../runner.js'; export default function rename(provider) { const access = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE : MSSQL_UPDATE; const isSqlite = provider?.dialect === 'sqlite3'; const touchSql = isSqlite @@ -33,7 +35,7 @@ export default function rename(provider) { } const cleaned = name.trim(); - const { languageId } = await access(googleAuthId, id, tx); + const { languageId, companyId } = await access(googleAuthId, id, tx); const slug = slugify(cleaned); @@ -50,6 +52,7 @@ export default function rename(provider) { } await runner.query(touchSql, { categoryId: id }); + await recordSync({ companyId, entityType: 'category', entityId: id }, runner); return { id, name: cleaned, slug }; }; diff --git a/src/store/categoryattributevalue/setByCategoryId.js b/src/store/categoryattributevalue/setByCategoryId.js index 76c9cdf..6255451 100644 --- a/src/store/categoryattributevalue/setByCategoryId.js +++ b/src/store/categoryattributevalue/setByCategoryId.js @@ -9,10 +9,12 @@ import { GET_VALUE_DETAIL as SQLITE_GET_DETAIL, } from '../../providers/sqlite3/queries/categoryattributevalue.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function setByCategoryId(provider) { const resolve = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT; @@ -140,6 +142,7 @@ export default function setByCategoryId(provider) { ? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId" : 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId'; await runner.query(touchSql, { categoryId }); + await recordSync({ companyId, entityType: 'category', entityId: categoryId }, runner); return inserted; }; diff --git a/src/store/categoryproperty/delete.js b/src/store/categoryproperty/delete.js index 227d575..7035de6 100644 --- a/src/store/categoryproperty/delete.js +++ b/src/store/categoryproperty/delete.js @@ -5,10 +5,12 @@ import { DELETE_BY_CATEGORY_AND_PROPERTY_ID as SQLITE_DELETE, } from '../../providers/sqlite3/queries/categoryproperty.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function deleteCategoryProperty(provider) { const resolve = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; @@ -23,7 +25,7 @@ export default function deleteCategoryProperty(provider) { */ return async (googleAuthId, categoryId, propertyId, tx) => { const runner = makeRunner(provider, tx); - await resolve(googleAuthId, categoryId, runner); + const { companyId } = await resolve(googleAuthId, categoryId, runner); await runner.query(deleteSql, { categoryId, propertyId }); @@ -32,5 +34,6 @@ export default function deleteCategoryProperty(provider) { ? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId" : 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId'; await runner.query(touchSql, { categoryId }); + await recordSync({ companyId, entityType: 'category', entityId: categoryId }, runner); }; } diff --git a/src/store/categoryproperty/setByCategoryId.js b/src/store/categoryproperty/setByCategoryId.js index 887100d..6c624e5 100644 --- a/src/store/categoryproperty/setByCategoryId.js +++ b/src/store/categoryproperty/setByCategoryId.js @@ -13,10 +13,12 @@ import { GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP, } from '../../providers/sqlite3/queries/propertyname.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function setByCategoryId(provider) { const resolve = resolveCategoryAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT; @@ -102,6 +104,7 @@ export default function setByCategoryId(provider) { ? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId" : 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId'; await runner.query(touchSql, { categoryId }); + await recordSync({ companyId, entityType: 'category', entityId: categoryId }, runner); return inserted; }; diff --git a/src/store/customergroup/create.js b/src/store/customergroup/create.js index 1fb7c3a..286836f 100644 --- a/src/store/customergroup/create.js +++ b/src/store/customergroup/create.js @@ -8,6 +8,7 @@ import { } from '../../providers/sqlite3/queries/customergroup.js'; import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; const RESERVED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]); @@ -18,6 +19,7 @@ function shape(row) { export default function create(provider) { const access = resolveSelectedCompanyAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; @@ -58,6 +60,7 @@ export default function create(provider) { }); const row = result.rows[0] ?? { id: result.insertId, name: cleaned, sort }; + await recordSync({ companyId, entityType: 'customergroup', entityId: row.id }, runner); return shape(row); }; } diff --git a/src/store/customergroup/order.js b/src/store/customergroup/order.js index 421f9ad..90ada4e 100644 --- a/src/store/customergroup/order.js +++ b/src/store/customergroup/order.js @@ -7,10 +7,12 @@ import { UPDATE_SORT as SQLITE_UPDATE_SORT, } from '../../providers/sqlite3/queries/customergroup.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function order(provider) { const access = resolveSelectedCompanyAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const listIdsSql = isSqlite ? SQLITE_LIST_IDS_BY_COMPANY @@ -65,6 +67,7 @@ export default function order(provider) { companyId, sort, }); + await recordSync({ companyId, entityType: 'customergroup', entityId: customergroupIds[sort] }, runner); } return { customergroupIds }; diff --git a/src/store/customergroup/update.js b/src/store/customergroup/update.js index b3e48f1..d7b041c 100644 --- a/src/store/customergroup/update.js +++ b/src/store/customergroup/update.js @@ -8,6 +8,7 @@ import { } from '../../providers/sqlite3/queries/customergroup.js'; import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; const LOCKED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]); @@ -18,6 +19,7 @@ function shape(row) { export default function update(provider) { const access = resolveSelectedCompanyAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSql = isSqlite ? SQLITE_GET_BY_COMPANY_AND_ID @@ -70,6 +72,7 @@ export default function update(provider) { }); const row = result.rows[0] ?? { id, name: cleaned, sort: current.sort }; + await recordSync({ companyId, entityType: 'customergroup', entityId: id }, runner); return shape(row); }; } diff --git a/src/store/entity/create.js b/src/store/entity/create.js index 0bfd158..6039d35 100644 --- a/src/store/entity/create.js +++ b/src/store/entity/create.js @@ -5,6 +5,7 @@ import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries import { isUniqueConstraintError } from '../../providers/provider.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import nextNumber from '../numbers/nextNumber.js'; import shape from './shape.js'; import makeRunner from '../runner.js'; @@ -12,6 +13,7 @@ import makeRunner from '../runner.js'; export default function create(provider) { const access = resolveSelectedCompanyAccess(provider); const groupAccess = resolveCustomerGroupAccess(provider); + const recordSync = recordSyncQueue(provider); const allocateNumber = nextNumber(provider); const isSqlite = provider?.dialect === 'sqlite3'; @@ -82,6 +84,8 @@ export default function create(provider) { text: 'Erstellt', }); + await recordSync({ companyId, entityType: 'customer', entityId: row.id }, runner); + return shape(row); } catch (err) { if (isUniqueConstraintError(err)) { diff --git a/src/store/entity/update.js b/src/store/entity/update.js index 6ecb702..66f61c6 100644 --- a/src/store/entity/update.js +++ b/src/store/entity/update.js @@ -10,11 +10,13 @@ import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/en import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/entityhistory.js'; import { isUniqueConstraintError } from '../../providers/provider.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import shape from './shape.js'; import makeRunner from '../runner.js'; export default function update(provider) { const access = resolveSelectedCompanyAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSql = isSqlite ? SQLITE_GET_BY_COMPANY_AND_ID @@ -134,6 +136,8 @@ export default function update(provider) { text: historyLines.join('\n'), }); + await recordSync({ companyId, entityType: 'customer', entityId: id }, runner); + return shape(row); } catch (err) { if (isUniqueConstraintError(err)) { diff --git a/src/store/entityaddress/create.js b/src/store/entityaddress/create.js index 5a7998b..c9d9635 100644 --- a/src/store/entityaddress/create.js +++ b/src/store/entityaddress/create.js @@ -6,8 +6,11 @@ import { CREATE as SQLITE_CREATE, GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, } from '../../providers/sqlite3/queries/entityaddress.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; function shape(row) { @@ -35,9 +38,11 @@ function optionalString(value) { export default function create(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Creates a new address for an entity. @@ -109,6 +114,9 @@ export default function create(provider) { sort, }; + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); + return shape(row); }; } diff --git a/src/store/entityaddress/delete.js b/src/store/entityaddress/delete.js index 547ef52..d4deaae 100644 --- a/src/store/entityaddress/delete.js +++ b/src/store/entityaddress/delete.js @@ -1,13 +1,19 @@ import { DELETE_BY_ENTITY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entityaddress.js'; import { DELETE_BY_ENTITY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entityaddress.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function del(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); - const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + const recordSync = recordSyncQueue(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Deletes an address of an entity. @@ -39,5 +45,8 @@ export default function del(provider) { if (!result.rowsAffected) { throw new Error('Address not found'); } + + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); }; } diff --git a/src/store/entityaddress/update.js b/src/store/entityaddress/update.js index cfbb0cb..cc88af9 100644 --- a/src/store/entityaddress/update.js +++ b/src/store/entityaddress/update.js @@ -6,8 +6,11 @@ import { GET_BY_ENTITY_AND_ID as SQLITE_GET_BY_ENTITY_AND_ID, UPDATE as SQLITE_UPDATE, } from '../../providers/sqlite3/queries/entityaddress.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; function shape(row) { @@ -35,11 +38,13 @@ function optionalString(value) { export default function update(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSql = isSqlite ? SQLITE_GET_BY_ENTITY_AND_ID : MSSQL_GET_BY_ENTITY_AND_ID; const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Updates an address of an entity. @@ -152,6 +157,9 @@ export default function update(provider) { sort, }; + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); + return shape(row); }; } diff --git a/src/store/entitycontact/create.js b/src/store/entitycontact/create.js index 5894a67..13d9c87 100644 --- a/src/store/entitycontact/create.js +++ b/src/store/entitycontact/create.js @@ -6,8 +6,11 @@ import { CREATE as SQLITE_CREATE, GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, } from '../../providers/sqlite3/queries/entitycontact.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; function shape(row) { @@ -23,9 +26,11 @@ function shape(row) { export default function create(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Creates a new contact for an entity. @@ -80,6 +85,9 @@ export default function create(provider) { sort, }; + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); + return shape(row); }; } diff --git a/src/store/entitycontact/delete.js b/src/store/entitycontact/delete.js index ae6d796..d744625 100644 --- a/src/store/entitycontact/delete.js +++ b/src/store/entitycontact/delete.js @@ -1,13 +1,19 @@ import { DELETE_BY_ENTITY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entitycontact.js'; import { DELETE_BY_ENTITY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entitycontact.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; export default function del(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); - const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + const recordSync = recordSyncQueue(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Deletes a contact of an entity. @@ -39,5 +45,8 @@ export default function del(provider) { if (!result.rowsAffected) { throw new Error('Contact not found'); } + + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); }; } diff --git a/src/store/entitycontact/update.js b/src/store/entitycontact/update.js index fc30271..91b1a19 100644 --- a/src/store/entitycontact/update.js +++ b/src/store/entitycontact/update.js @@ -6,8 +6,11 @@ import { GET_BY_ENTITY_AND_ID as SQLITE_GET_BY_ENTITY_AND_ID, UPDATE as SQLITE_UPDATE, } from '../../providers/sqlite3/queries/entitycontact.js'; +import { TOUCH_UPDATED_AT as MSSQL_TOUCH_ENTITY } from '../../providers/mssql/queries/entity.js'; +import { TOUCH_UPDATED_AT as SQLITE_TOUCH_ENTITY } from '../../providers/sqlite3/queries/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; +import recordSyncQueue from '../posterminal/recordSyncQueue.js'; import makeRunner from '../runner.js'; function shape(row) { @@ -23,11 +26,13 @@ function shape(row) { export default function update(provider) { const access = resolveSelectedCompanyAccess(provider); const entityAccess = resolveEntityAccess(provider); + const recordSync = recordSyncQueue(provider); const isSqlite = provider?.dialect === 'sqlite3'; const getSql = isSqlite ? SQLITE_GET_BY_ENTITY_AND_ID : MSSQL_GET_BY_ENTITY_AND_ID; const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + const touchEntitySql = isSqlite ? SQLITE_TOUCH_ENTITY : MSSQL_TOUCH_ENTITY; /** * Updates a contact of an entity. @@ -100,6 +105,9 @@ export default function update(provider) { sort, }; + await runner.query(touchEntitySql, { id: entityId }); + await recordSync({ companyId, entityType: 'customer', entityId }, runner); + return shape(row); }; } diff --git a/src/store/ledger/add.js b/src/store/ledger/add.js index 34330a3..b3c7686 100644 --- a/src/store/ledger/add.js +++ b/src/store/ledger/add.js @@ -14,10 +14,14 @@ import makeRunner from '../runner.js'; import { TOUCH_UPDATED_AT as MSSQL_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, + ENQUEUE_STUECKLISTE_ARTICLES_SYNC as MSSQL_ENQUEUE_STUECKLISTE_SYNC, } from '../../providers/mssql/queries/article.js'; import { TOUCH_UPDATED_AT as SQLITE_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, + ENQUEUE_STUECKLISTE_ARTICLES_SYNC as SQLITE_ENQUEUE_STUECKLISTE_SYNC, } from '../../providers/sqlite3/queries/article.js'; export const INIT_HASH = 'TODO_TSE_BASED_INITHASH'; @@ -218,6 +222,12 @@ export default function add(provider) { const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE; await runner.query(touchSql, { articleId: match.id }); await runner.query(touchStuecklisteSql, { articleId: match.id }); + if (!isTracked) { + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; + const enqueueStuecklisteSyncSql = isSqlite ? SQLITE_ENQUEUE_STUECKLISTE_SYNC : MSSQL_ENQUEUE_STUECKLISTE_SYNC; + await runner.query(enqueueSyncSql, { articleId: match.id }); + await runner.query(enqueueStuecklisteSyncSql, { articleId: match.id }); + } await recordAcceptanceHistory(runner, { articleId: match.id, companyuserId, @@ -312,6 +322,12 @@ export default function add(provider) { const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE; await runner.query(touchSql, { articleId: match.id }); await runner.query(touchStuecklisteSql, { articleId: match.id }); + if (!isTracked) { + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; + const enqueueStuecklisteSyncSql = isSqlite ? SQLITE_ENQUEUE_STUECKLISTE_SYNC : MSSQL_ENQUEUE_STUECKLISTE_SYNC; + await runner.query(enqueueSyncSql, { articleId: match.id }); + await runner.query(enqueueStuecklisteSyncSql, { articleId: match.id }); + } await recordShipmentHistory(runner, { articleId: match.id, companyuserId, diff --git a/src/store/optionslist/setOptionsByArticleSku.js b/src/store/optionslist/setOptionsByArticleSku.js index 5970f95..9486b04 100644 --- a/src/store/optionslist/setOptionsByArticleSku.js +++ b/src/store/optionslist/setOptionsByArticleSku.js @@ -1,7 +1,13 @@ import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js'; import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js'; -import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js'; -import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js'; +import { + TOUCH_UPDATED_AT as MSSQL_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as MSSQL_ENQUEUE_SYNC, +} from '../../providers/mssql/queries/article.js'; +import { + TOUCH_UPDATED_AT as SQLITE_TOUCH, + ENQUEUE_SYNC_BY_ARTICLE as SQLITE_ENQUEUE_SYNC, +} from '../../providers/sqlite3/queries/article.js'; import resolveArticleBySku from '../article/resolveArticleBySku.js'; import getOptionsByArticleSku from './getOptionsByArticleSku.js'; import { @@ -49,6 +55,7 @@ export default function setOptionsByArticleSku(provider) { const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteQueries : mssqlQueries; const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const enqueueSyncSql = isSqlite ? SQLITE_ENQUEUE_SYNC : MSSQL_ENQUEUE_SYNC; /** * Sets or clears the options list for an article. @@ -112,6 +119,7 @@ export default function setOptionsByArticleSku(provider) { }); } await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); } return []; } @@ -242,6 +250,7 @@ export default function setOptionsByArticleSku(provider) { // Touch article updated_at so POS and other consumers pick up options/deposit changes await runner.query(touchArticleSql, { articleId }); + await runner.query(enqueueSyncSql, { articleId }); return result; }; diff --git a/src/store/posterminal/getCategoryCount.js b/src/store/posterminal/getCategoryCount.js index 5476b71..9bf179c 100644 --- a/src/store/posterminal/getCategoryCount.js +++ b/src/store/posterminal/getCategoryCount.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCategoryCount(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Counts categories assigned to the POS terminal updated after the given unix timestamp cursor. + * Counts categories assigned to the POS terminal queued after the given sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The count of changed records. */ diff --git a/src/store/posterminal/getCategoryList.js b/src/store/posterminal/getCategoryList.js index 0222f0e..c299c5d 100644 --- a/src/store/posterminal/getCategoryList.js +++ b/src/store/posterminal/getCategoryList.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCategoryList(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Lists categories for a POS terminal modified after the cursor. + * Lists categories for a POS terminal queued after the sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {number} [input.limit=20] - Max items to return. * @param {object} [tx] - Optional transaction. * @returns {Promise>} diff --git a/src/store/posterminal/getCompositeProductCount.js b/src/store/posterminal/getCompositeProductCount.js index ebacda1..8df9a31 100644 --- a/src/store/posterminal/getCompositeProductCount.js +++ b/src/store/posterminal/getCompositeProductCount.js @@ -4,21 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - if (n < 1e11) return Math.floor(n * 1000000); - if (n < 1e14) return Math.floor((n / 1000) * 1000000); - return Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCompositeProductCount(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor. + * Counts composite products (Stücklisten) assigned to the POS terminal queued after the given sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The count of changed records. */ diff --git a/src/store/posterminal/getCustomerCount.js b/src/store/posterminal/getCustomerCount.js index 7fa1162..c3f6c28 100644 --- a/src/store/posterminal/getCustomerCount.js +++ b/src/store/posterminal/getCustomerCount.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCustomerCount(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Counts customers of the POS terminal company updated after the given unix timestamp cursor. + * Counts customers of the POS terminal company queued after the given sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The count of changed records. */ diff --git a/src/store/posterminal/getCustomerGroupCount.js b/src/store/posterminal/getCustomerGroupCount.js index f8ffa8b..390df86 100644 --- a/src/store/posterminal/getCustomerGroupCount.js +++ b/src/store/posterminal/getCustomerGroupCount.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCustomerGroupCount(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Counts customer groups of the POS terminal company updated after the given unix timestamp cursor. + * Counts customer groups of the POS terminal company queued after the given sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The count of changed records. */ diff --git a/src/store/posterminal/getCustomerGroupList.js b/src/store/posterminal/getCustomerGroupList.js index caef445..1f84679 100644 --- a/src/store/posterminal/getCustomerGroupList.js +++ b/src/store/posterminal/getCustomerGroupList.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCustomerGroupList(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Lists customer groups for a POS terminal modified after the cursor, excluding 'Lieferanten'. + * Lists customer groups for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {number} [input.limit=50] - Max items to return. * @param {object} [tx] - Optional transaction. * @returns {Promise>} diff --git a/src/store/posterminal/getCustomerList.js b/src/store/posterminal/getCustomerList.js index 3cb83e0..2a2ec64 100644 --- a/src/store/posterminal/getCustomerList.js +++ b/src/store/posterminal/getCustomerList.js @@ -4,19 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getCustomerList(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Lists customers for a POS terminal modified after the cursor, excluding 'Lieferanten'. + * Lists customers for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {number} [input.limit=20] - Max items to return. * @param {object} [tx] - Optional transaction. * @returns {Promise>} diff --git a/src/store/posterminal/getProductCount.js b/src/store/posterminal/getProductCount.js index 85372bc..5047b07 100644 --- a/src/store/posterminal/getProductCount.js +++ b/src/store/posterminal/getProductCount.js @@ -4,21 +4,18 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - if (n < 1e11) return Math.floor(n * 1000000); - if (n < 1e14) return Math.floor((n / 1000) * 1000000); - return Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } export default function getProductCount(provider) { const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; /** - * Counts products assigned to the POS terminal updated after the given unix timestamp cursor. + * Counts products assigned to the POS terminal queued after the given sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The count of changed records. */ diff --git a/src/store/posterminal/getProductList.js b/src/store/posterminal/getProductList.js index 191dcf6..25318c6 100644 --- a/src/store/posterminal/getProductList.js +++ b/src/store/posterminal/getProductList.js @@ -18,10 +18,7 @@ import makeRunner from '../runner.js'; function normalizeCursor(cursor) { const n = Number(cursor) || 0; - if (n <= 0) return 0; - if (n < 1e11) return Math.floor(n * 1000000); - if (n < 1e14) return Math.floor((n / 1000) * 1000000); - return Math.floor(n); + return n > 0 ? Math.floor(n) : 0; } function formatDateTime(value) { @@ -46,11 +43,11 @@ export default function getProductList(provider) { const parentOptionsSql = isSqlite ? SQLITE_PARENT_OPTIONS : MSSQL_PARENT_OPTIONS; /** - * Lists products for a POS terminal modified after the cursor. + * Lists products for a POS terminal queued after the sync queue cursor ID. * * @param {object} input * @param {number} input.posterminalId - The POS terminal id. - * @param {number} [input.cursor=0] - Unix timestamp cursor. + * @param {number} [input.cursor=0] - Sequential ID cursor of last synced posterminalsyncqueue entry. * @param {number} [input.limit=20] - Max items to return. * @param {object} [tx] - Optional transaction. * @returns {Promise>} diff --git a/src/store/posterminal/recordSyncQueue.js b/src/store/posterminal/recordSyncQueue.js new file mode 100644 index 0000000..1915021 --- /dev/null +++ b/src/store/posterminal/recordSyncQueue.js @@ -0,0 +1,28 @@ +import { RECORD_SYNC_QUEUE as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { RECORD_SYNC_QUEUE as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +export default function recordSyncQueue(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Enqueues a sync notification for all POS terminals of a company. + * + * @param {object} input + * @param {number} input.companyId - The company id. + * @param {string} input.entityType - 'category' | 'article' | 'customer' | 'customergroup' + * @param {number} input.entityId - The entity's id. + * @param {object} [tx] - Optional transaction/runner. + * @returns {Promise} + */ + return async ({ companyId, entityType, entityId }, tx) => { + if (!companyId || !entityType || !entityId) return; + const runner = makeRunner(provider, tx); + await runner.query(sql, { + companyId: Number(companyId), + entityType: String(entityType).toLowerCase().trim(), + entityId: Number(entityId), + }); + }; +} + diff --git a/src/store/posterminal/setOff.js b/src/store/posterminal/setOff.js index 55430fa..10b7c1e 100644 --- a/src/store/posterminal/setOff.js +++ b/src/store/posterminal/setOff.js @@ -8,6 +8,14 @@ import { SET_OFF as SQLITE_SET_OFF, LIST_CATEGORY_IDS as SQLITE_LIST_CATEGORY_IDS, } from '../../providers/sqlite3/queries/posterminal.js'; +import { + ENQUEUE_SUBTREE_CATEGORIES_SYNC as MSSQL_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as MSSQL_ENQUEUE_SUBTREE_ARTICLES, +} from '../../providers/mssql/queries/category.js'; +import { + ENQUEUE_SUBTREE_CATEGORIES_SYNC as SQLITE_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as SQLITE_ENQUEUE_SUBTREE_ARTICLES, +} from '../../providers/sqlite3/queries/category.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -20,6 +28,12 @@ export default function setOff(provider) { const listCatIdsSql = isSqlite ? SQLITE_LIST_CATEGORY_IDS : MSSQL_LIST_CATEGORY_IDS; + const enqueueCategoriesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_CATEGORIES + : MSSQL_ENQUEUE_SUBTREE_CATEGORIES; + const enqueueArticlesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_ARTICLES + : MSSQL_ENQUEUE_SUBTREE_ARTICLES; const touchSql = isSqlite ? `WITH RECURSIVE cats AS ( @@ -182,6 +196,8 @@ export default function setOff(provider) { await runner.query(touchSql, { categoryId }); await runner.query(touchArticlesSql, { categoryId }); + await runner.query(enqueueCategoriesSql, { id: categoryId, companyId }); + await runner.query(enqueueArticlesSql, { id: categoryId, companyId }); const result = await runner.query(listCatIdsSql, { posterminalId }); diff --git a/src/store/posterminal/setOn.js b/src/store/posterminal/setOn.js index 51a3e70..2d2f82e 100644 --- a/src/store/posterminal/setOn.js +++ b/src/store/posterminal/setOn.js @@ -8,6 +8,14 @@ import { SET_ON as SQLITE_SET_ON, LIST_CATEGORY_IDS as SQLITE_LIST_CATEGORY_IDS, } from '../../providers/sqlite3/queries/posterminal.js'; +import { + ENQUEUE_SUBTREE_CATEGORIES_SYNC as MSSQL_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as MSSQL_ENQUEUE_SUBTREE_ARTICLES, +} from '../../providers/mssql/queries/category.js'; +import { + ENQUEUE_SUBTREE_CATEGORIES_SYNC as SQLITE_ENQUEUE_SUBTREE_CATEGORIES, + ENQUEUE_SUBTREE_ARTICLES_SYNC as SQLITE_ENQUEUE_SUBTREE_ARTICLES, +} from '../../providers/sqlite3/queries/category.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -20,6 +28,12 @@ export default function setOn(provider) { const listCatIdsSql = isSqlite ? SQLITE_LIST_CATEGORY_IDS : MSSQL_LIST_CATEGORY_IDS; + const enqueueCategoriesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_CATEGORIES + : MSSQL_ENQUEUE_SUBTREE_CATEGORIES; + const enqueueArticlesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_ARTICLES + : MSSQL_ENQUEUE_SUBTREE_ARTICLES; const touchSql = isSqlite ? `WITH RECURSIVE cats AS ( SELECT id FROM category WHERE id = @categoryId @@ -166,6 +180,8 @@ export default function setOn(provider) { await runner.query(touchSql, { categoryId }); await runner.query(touchArticlesSql, { categoryId }); + await runner.query(enqueueCategoriesSql, { id: categoryId, companyId }); + await runner.query(enqueueArticlesSql, { id: categoryId, companyId }); const result = await runner.query(listCatIdsSql, { posterminalId }); diff --git a/src/store/posterminal/setOnAll.js b/src/store/posterminal/setOnAll.js index 9637b2c..06bde8d 100644 --- a/src/store/posterminal/setOnAll.js +++ b/src/store/posterminal/setOnAll.js @@ -10,6 +10,8 @@ import { } from '../../providers/sqlite3/queries/posterminal.js'; import { GET_COMPANY_ROOT_CATEGORY as MSSQL_ROOT } from '../../providers/mssql/queries/company.js'; import { GET_COMPANY_ROOT_CATEGORY as SQLITE_ROOT } from '../../providers/sqlite3/queries/company.js'; +import { ENQUEUE_SUBTREE_CATEGORIES_SYNC as MSSQL_ENQUEUE_SUBTREE_CATEGORIES } from '../../providers/mssql/queries/category.js'; +import { ENQUEUE_SUBTREE_CATEGORIES_SYNC as SQLITE_ENQUEUE_SUBTREE_CATEGORIES } from '../../providers/sqlite3/queries/category.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -23,6 +25,16 @@ export default function setOnAll(provider) { ? SQLITE_LIST_CATEGORY_IDS : MSSQL_LIST_CATEGORY_IDS; const rootSql = isSqlite ? SQLITE_ROOT : MSSQL_ROOT; + const enqueueCategoriesSql = isSqlite + ? SQLITE_ENQUEUE_SUBTREE_CATEGORIES + : MSSQL_ENQUEUE_SUBTREE_CATEGORIES; + const enqueueAllArticlesSql = ` + INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) + SELECT pt.id, 'article', a.id + FROM article a + INNER JOIN posterminal pt ON pt.company_id = a.company_id + WHERE a.company_id = @companyId + `; const touchAllSql = isSqlite ? `WITH RECURSIVE cats AS ( SELECT id FROM category WHERE id = @rootCategoryId @@ -100,6 +112,8 @@ export default function setOnAll(provider) { await runner.query(touchAllSql, { rootCategoryId }); await runner.query(touchAllArticlesSql, { companyId }); + await runner.query(enqueueCategoriesSql, { id: rootCategoryId, companyId }); + await runner.query(enqueueAllArticlesSql, { companyId }); const result = await runner.query(listCatIdsSql, { posterminalId }); diff --git a/src/store/posterminal/upsert.js b/src/store/posterminal/upsert.js index f4f3002..dc1d806 100644 --- a/src/store/posterminal/upsert.js +++ b/src/store/posterminal/upsert.js @@ -2,11 +2,13 @@ import { GET_BY_ID as MSSQL_GET_BY_ID, CREATE as MSSQL_CREATE, UPDATE as MSSQL_UPDATE, + SEED_SYNC_QUEUE_FOR_TERMINAL as MSSQL_SEED_SYNC_QUEUE, } from '../../providers/mssql/queries/posterminal.js'; import { GET_BY_ID as SQLITE_GET_BY_ID, CREATE as SQLITE_CREATE, UPDATE as SQLITE_UPDATE, + SEED_SYNC_QUEUE_FOR_TERMINAL as SQLITE_SEED_SYNC_QUEUE, } from '../../providers/sqlite3/queries/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -18,6 +20,7 @@ export default function upsert(provider) { const getByIdSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + const seedSyncQueueSql = isSqlite ? SQLITE_SEED_SYNC_QUEUE : MSSQL_SEED_SYNC_QUEUE; /** * Creates or updates a posterminal for the user's selected company. @@ -143,6 +146,11 @@ export default function upsert(provider) { lastseenAt, }; + await runner.query(seedSyncQueueSql, { + posterminalId: row.id, + companyId, + }); + return { id: row.id, name: row.name,