diff --git a/README.md b/README.md index 652c842..3dfcde1 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,37 @@ Company-scoped brands. Auth via `resolveCompanyAccess`. --- +## `posterminal` + +Company-scoped POS terminals. Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `{ id, token, pairingpin }[]` | +| `upsert` | `(googleAuthId, companyId, { id?, token?, pairingpin })` | `{ id, token, pairingpin }` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `setOn` | `(googleAuthId, companyId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | +| `setOff` | `(googleAuthId, companyId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | +| `setOnAll` | `(googleAuthId, companyId, posterminalId)` | `{ id, categoryIds[] }` | +| `setOffAll` | `(googleAuthId, companyId, posterminalId)` | `{ id, categoryIds[] }` | +| `getCategoryPosterminalIds` | `(companyId)` | `{ [categoryId]: posterminalId[] }` (internal, used by category tree) | + +`posterminal.company_id` **NOT NULL**. `token` is nullable and unique (pairing code assigned during setup). `pairingpin` is required. + +`upsert`: if `id` is provided, updates the existing posterminal (must belong to company); otherwise creates a new one. Errors: `Company not found`, `Not authorized`, `Invalid posterminal`, `Posterminal not found`. + +`setOn` / `setOff`: toggle a single category's visibility on a single terminal. `setOn` inserts the row (idempotent — no error if already present); `setOff` deletes it (idempotent). Both return the updated `categoryIds` for that terminal. Errors: `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal or category`. + +`setOnAll` / `setOffAll`: toggle **all** company categories on a single terminal. `setOnAll` walks the company's category tree and inserts any missing links; `setOffAll` removes all category links for the terminal. Both return the updated `categoryIds` for that terminal. Errors: `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal`. + +`delete` cascades to `posterminalcategory`. Errors: `Company not found`, `Not authorized`, `Posterminal not found`. + +### Category tree integration + +`getTreeForCompany` returns a `posterminalIds` array on each tree node — the list of posterminal ids (of the current company) for which that category is visible. A category with an empty `posterminalIds` array is visible on all terminals (or has no terminal-specific filtering). + +--- + ## `category` | Method | Signature | Returns | @@ -335,6 +366,13 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `GET /api/brand?companyId=` | `brand.list(googleAuthId, companyId)` | | `PUT /api/brand` | `brand.upsert(googleAuthId, companyId, { name, gsprpinfo? })` | | `DELETE /api/brand/:id` | `brand.delete(googleAuthId, companyId, id)` | +| `GET /api/posterminal?companyId=` | `posterminal.list(googleAuthId, companyId)` | +| `PUT /api/posterminal` | `posterminal.upsert(googleAuthId, companyId, { id?, token?, pairingpin })` | +| `DELETE /api/posterminal/:id` | `posterminal.delete(googleAuthId, companyId, id)` | +| `PUT /api/posterminal/:id/category/:categoryId` | `posterminal.setOn(googleAuthId, companyId, id, categoryId)` | +| `DELETE /api/posterminal/:id/category/:categoryId` | `posterminal.setOff(googleAuthId, companyId, id, categoryId)` | +| `PUT /api/posterminal/:id/categories` | `posterminal.setOnAll(googleAuthId, companyId, id)` | +| `DELETE /api/posterminal/:id/categories` | `posterminal.setOffAll(googleAuthId, companyId, id)` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | diff --git a/schema-graph.dot b/schema-graph.dot index f341dee..e7e16a6 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -12,7 +12,7 @@ digraph schema { numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; entity [label=<
entity
idINT PK
customergroup_idINT FK
firstnameNVARCHAR(255)
lastnameNVARCHAR(255)
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; - company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; + company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; @@ -33,11 +33,11 @@ digraph schema { style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - article [label=<
article
idINT PK
unit_idINT FK
fatherarticle_id?INT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; - brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; + brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + article [label=<
article
idINT PK
brand_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; articlelist [label=<
articlelist
idINT PK
>]; articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; optionslist [label=<
optionslist
idINT PK
>]; @@ -67,30 +67,32 @@ digraph schema { fontcolor="#1a365d"; margin=14; unit [label=<
unit
idINT PK
company_idINT FK
siNVARCHAR(50)
basesiNVARCHAR(50)
basesifactorFLOAT
>]; } + posterminalcategory [label=<
posterminalcategory
idINT PK
posterminal_idINT FK
category_idINT FK
>]; + posterminal [label=<
posterminal
idINT PK
company_idINT FK
token?NVARCHAR(255)
pairingpinNVARCHAR(50)
>]; __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:sw -> company:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; + google_auth:selected_company:w -> company:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; category:parent_id:e -> category:id:e [color="#2f855a", fontcolor="#2f855a", tooltip="FK_category_parent"]; - company:root_category_id:w -> category:in4:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; + company:root_category_id:w -> category:in5:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; company:default_language_id:w -> language:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; company:taxrate:e -> tax:in2:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate"]; company:reducedtaxrate:e -> tax:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; company:taxfreerate:e -> tax:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; - customergroup:company_id:nw -> company:in4:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; + customergroup:company_id:nw -> company:in5:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; company:poscustomergroup_id:se -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; entity:customergroup_id:e -> customergroup:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; - unit:company_id:e -> company:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_unit_company"]; + unit:company_id:e -> company:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_unit_company"]; unittext:unit_id:e -> unit:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_unittext_unit"]; unittext:language_id:sw -> language:in1:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unittext_language"]; brand:company_id:e -> company:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_brand_company"]; - article:unit_id:w -> unit:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_article_unit"]; - article:brand_id:se -> brand:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_article_brand"]; + article:unit_id:e -> unit:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_article_unit"]; + article:brand_id:sw -> brand:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_article_brand"]; article:articlelist_id:se -> articlelist:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_article_articlelist"]; article:optionslist_id:se -> optionslist:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_optionslist"]; article:fatherarticle_id:e -> article:id:e [color="#285e61", fontcolor="#285e61", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:se -> article:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlehistory_article"]; - articleean:article_id:se -> article:in3:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articleean_article"]; + articlehistory:article_id:e -> article:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlehistory_article"]; + articleean:article_id:se -> article:in4:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articleean_article"]; articlepic:article_id:e -> article:in1:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlepic_article"]; articlepic:pic_id:sw -> pic:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlepic_pic"]; brandpic:brand_id:e -> brand:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_brandpic_brand"]; @@ -98,24 +100,27 @@ digraph schema { articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlelistarticle_articlelist"]; articlelistarticle:article_id:sw -> article:in6:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlelistarticle_article"]; optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:w -> article:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:sw -> category:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:sw -> article:in5:n [color="#319795", fontcolor="#319795", tooltip="FK_categoryarticle_article"]; - categorypic:category_id:se -> category:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categorypic_category"]; + optionslistarticle:article_id:sw -> article:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in4:n [color="#c05621", fontcolor="#c05621", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:se -> article:in3:n [color="#319795", fontcolor="#319795", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:se -> category:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categorypic_category"]; categorypic:pic_id:se -> pic:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categorypic_pic"]; categorytext:category_id:e -> category:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_categorytext_category"]; categorytext:language_id:sw -> language:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorytext_language"]; articletext:article_id:e -> article:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_articletext_article"]; - articletext:language_id:se -> language:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articletext_language"]; + articletext:language_id:sw -> language:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articletext_language"]; price:tax_id:sw -> tax:in3:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_price_tax"]; price:article_id:w -> article:in8:n [color="#22543d", fontcolor="#22543d", tooltip="FK_price_article"]; price:customergroup_id:sw -> customergroup:in2:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_price_customergroup"]; manifestarticle:manifest_id:sw -> manifest:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_manifestarticle_manifest"]; manifestarticle:article_id:w -> article:in9:n [color="#744210", fontcolor="#744210", tooltip="FK_manifestarticle_article"]; manifestarticle:tax_id:sw -> tax:in4:n [color="#285e61", fontcolor="#285e61", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_numbers_company"]; + numbers:company_id:se -> company:in3:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_numbers_company"]; companyuser:google_auth_id:se -> google_auth:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:sw -> company:in6:n [color="#38a169", fontcolor="#38a169", tooltip="FK_companyuser_company"]; - syncpos:company_id:se -> company:in3:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_syncpos_company"]; - companysetting:company_id:sw -> company:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_companysetting_company"]; + companyuser:company_id:sw -> company:in7:n [color="#38a169", fontcolor="#38a169", tooltip="FK_companyuser_company"]; + syncpos:company_id:se -> company:in4:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in6:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_companysetting_company"]; + posterminal:company_id:e -> company:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_posterminal_company"]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_posterminalcategory_posterminal"]; + posterminalcategory:category_id:se -> category:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_posterminalcategory_category"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index d7e6295..f661f3e 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,232 +4,233 @@ - + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_commerce - -Pricing & Documents + +Pricing & Documents cluster_units - -Units + +Units 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 company - - - - - - - - - - -company - - -id -INT -PK - - -default_language_id -? -INT -FK - - -root_category_id -? -INT -FK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + +company + + +id +INT +PK + + +default_language_id +? +INT +FK + + +root_category_id +? +INT +FK + + +poscustomergroup_id +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +name +NVARCHAR(255) numbers:se->company:n - - + + entity - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -firstname -NVARCHAR(255) - - -lastname -NVARCHAR(255) + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +firstname +NVARCHAR(255) + + +lastname +NVARCHAR(255) customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) entity:e->customergroup:n - - + + syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) syncpos:se->company:n - - + + @@ -237,103 +238,104 @@ company:se->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 + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT company:w->category:n - - + + tax - - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) company:e->tax:n - - + + @@ -341,8 +343,8 @@ company:e->tax:n - - + + @@ -350,407 +352,407 @@ company:e->tax:n - - + + meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) customergroup:nw->company:n - - + + companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT companysetting:sw->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 companyuser:sw->company:n - - + + google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -created_at -DATETIME2 + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +created_at +DATETIME2 companyuser:se->google_auth:n - - + + -google_auth:sw->company:n +google_auth:w->company: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 + + +id +INT +PK + + +language_id +INT +FK + + +article_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) -articletext:se->language:n +articletext:sw->language:n - - + + - + article - - - - - - - - - - - - -article - - -id -INT -PK - - -unit_id -INT -FK - - -fatherarticle_id -? -INT -FK - - -brand_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + +article + + +id +INT +PK + + +brand_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +unit_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 articletext:e->article: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:sw->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:e->unit: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 + + +id +INT +PK + + +language_id +INT +FK + + +category_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) categorytext:sw->language:n - - + + @@ -758,42 +760,42 @@ categorytext:e->category:n - - + + articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) -articlehistory:se->article:n +articlehistory:e->article:n - - + + @@ -801,38 +803,123 @@ category:e->category:e - - + + + + + + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK + + + +categoryarticle:sw->category:n + + + + + + + + +categoryarticle:se->article:n + + + + + + + + +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +brand:e->company:n + + + - + articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) articleean:se->article:n - - + + + + + + + +article:sw->brand:n + + + @@ -840,187 +927,102 @@ article:e->article:e - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:se->brand:n - - - + + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK article:se->articlelist:n - - + + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK article:se->optionslist:n - - + + -article:w->unit:n +article:e->unit:n - - - - - - - -brand:e->company:n - - - - - - - - -categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK - - - -categoryarticle:sw->category:n - - - - - - - - -categoryarticle:sw->article:n - - - + + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT articlelistarticle:sw->article:n - - + + @@ -1028,51 +1030,51 @@ articlelistarticle:sw->articlelist:n - - + + optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT -optionslistarticle:w->article:n +optionslistarticle:sw->article:n - - + + @@ -1080,142 +1082,142 @@ optionslistarticle:sw->optionslist:n - - + + categorypic - - -categorypic - - -id -INT -PK - - -pic_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT categorypic:se->category:n - - + + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) categorypic:se->pic:n - - + + articlepic - - -articlepic - - -id -INT -PK - - -pic_id -INT -FK - - -article_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +pic_id +INT +FK + + +article_id +INT +FK + + +sort +INT articlepic:e->article:n - - + + @@ -1223,43 +1225,43 @@ articlepic:sw->pic:n - - + + brandpic - - -brandpic - - -id -INT -PK - - -pic_id -INT -FK - - -brand_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +pic_id +INT +FK + + +brand_id +INT +FK + + +sort +INT brandpic:e->brand:n - - + + @@ -1267,52 +1269,52 @@ brandpic:sw->pic:n - - + + price - - -price - - -id -INT -PK - - -article_id -INT -FK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + +price + + +id +INT +PK + + +article_id +INT +FK + + +customergroup_id +INT +FK + + +tax_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT price:sw->customergroup:n - - + + @@ -1320,8 +1322,8 @@ price:w->article:n - - + + @@ -1329,90 +1331,90 @@ price:sw->tax:n - - + + manifest - - - -manifest - - -id -INT -PK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 + + + +manifest + + +id +INT +PK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 manifestarticle - - -manifestarticle - - -id -INT -PK - - -article_id -? -INT -FK - - -tax_id -INT -FK - - -manifest_id -INT -FK - - -amount -FLOAT - - -net -FLOAT - - -transactedamount -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT + + +manifestarticle + + +id +INT +PK + + +article_id +? +INT +FK + + +tax_id +INT +FK + + +manifest_id +INT +FK + + +amount +FLOAT + + +net +FLOAT + + +transactedamount +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT manifestarticle:w->article:n - - + + @@ -1420,8 +1422,8 @@ manifestarticle:sw->tax:n - - + + @@ -1429,8 +1431,8 @@ manifestarticle:sw->manifest:n - - + + @@ -1438,38 +1440,113 @@ unit:e->company:n - - + + + + + + + +posterminalcategory + + +posterminalcategory + + +id +INT +PK + + +posterminal_id +INT +FK + + +category_id +INT +FK + + + +posterminalcategory:se->category:n + + + + + + + + +posterminal + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) + + + +posterminalcategory:se->posterminal:n + + + + + + + + +posterminal:e->company: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/src/index.js b/src/index.js index c491aa3..93b4273 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,14 @@ import companysettingDelete from './store/companysetting/delete.js'; import brandList from './store/brand/list.js'; import brandUpsert from './store/brand/upsert.js'; import brandDelete from './store/brand/delete.js'; +import posterminalList from './store/posterminal/list.js'; +import posterminalUpsert from './store/posterminal/upsert.js'; +import posterminalDelete from './store/posterminal/delete.js'; +import posterminalSetOn from './store/posterminal/setOn.js'; +import posterminalSetOff from './store/posterminal/setOff.js'; +import posterminalSetOnAll from './store/posterminal/setOnAll.js'; +import posterminalSetOffAll from './store/posterminal/setOffAll.js'; +import posterminalGetCategoryPosterminalIds from './store/posterminal/getCategoryPosterminalIds.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -129,6 +137,17 @@ export default class LibDashStore { delete: brandDelete(pool), }; + this.posterminal = { + list: posterminalList(pool), + upsert: posterminalUpsert(pool), + delete: posterminalDelete(pool), + setOn: posterminalSetOn(pool), + setOff: posterminalSetOff(pool), + setOnAll: posterminalSetOnAll(pool), + setOffAll: posterminalSetOffAll(pool), + getCategoryPosterminalIds: posterminalGetCategoryPosterminalIds(pool), + }; + this.google_auth = { add: googleAuthAdd(pool), getByEmail: googleAuthGetByEmail(pool), diff --git a/src/schema.js b/src/schema.js index 75aaa0c..36d7f4e 100644 --- a/src/schema.js +++ b/src/schema.js @@ -568,6 +568,34 @@ BEGIN END `; +export const INIT_POSTERMINAL = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminal') +BEGIN + CREATE TABLE posterminal ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + token NVARCHAR(255) NULL, + pairingpin NVARCHAR(50) NOT NULL, + CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_posterminal_token UNIQUE (token) + ); +END +`; + +export const INIT_POSTERMINALCATEGORY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminalcategory') +BEGIN + CREATE TABLE posterminalcategory ( + id INT IDENTITY(1,1) PRIMARY KEY, + posterminal_id INT NOT NULL, + category_id INT NOT NULL, + CONSTRAINT FK_posterminalcategory_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE, + CONSTRAINT FK_posterminalcategory_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE NO ACTION, + CONSTRAINT UQ_posterminalcategory_posterminal_category UNIQUE (posterminal_id, category_id) + ); +END +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -604,4 +632,6 @@ export const SCHEMA_SCRIPTS = [ INIT_COMPANYUSER, INIT_SYNCPOS, INIT_COMPANYSETTING, + INIT_POSTERMINAL, + INIT_POSTERMINALCATEGORY, ]; diff --git a/src/sql/company.js b/src/sql/company.js index 597aa52..87f925f 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -10,6 +10,10 @@ FROM company WHERE id = @id `; +export const GET_COMPANY_ROOT_CATEGORY = ` +SELECT root_category_id AS rootCategoryId FROM company WHERE id = @companyId +`; + export const CREATE = ` INSERT INTO company ( name, diff --git a/src/sql/posterminal.js b/src/sql/posterminal.js new file mode 100644 index 0000000..7545fa9 --- /dev/null +++ b/src/sql/posterminal.js @@ -0,0 +1,89 @@ +export const LIST_BY_COMPANY = ` +SELECT + id, + token, + pairingpin +FROM posterminal +WHERE company_id = @companyId +ORDER BY id +`; + +export const GET_BY_ID = ` +SELECT + id, + company_id AS companyId, + token, + pairingpin +FROM posterminal +WHERE id = @id +`; + +export const CREATE = ` +INSERT INTO posterminal (company_id, token, pairingpin) +OUTPUT INSERTED.id, INSERTED.token, INSERTED.pairingpin +VALUES (@companyId, @token, @pairingpin) +`; + +export const UPDATE = ` +UPDATE posterminal +SET token = @token, pairingpin = @pairingpin +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM posterminal +WHERE id = @id AND company_id = @companyId +`; + +export const LIST_CATEGORY_IDS = ` +SELECT category_id AS categoryId +FROM posterminalcategory +WHERE posterminal_id = @posterminalId +`; + +/** setOn: insert if not exists (single terminal + single category). */ +export const SET_ON = ` +IF NOT EXISTS ( + SELECT 1 FROM posterminalcategory + WHERE posterminal_id = @posterminalId AND category_id = @categoryId +) +INSERT INTO posterminalcategory (posterminal_id, category_id) +VALUES (@posterminalId, @categoryId) +`; + +/** setOff: remove a single category from a single terminal. */ +export const SET_OFF = ` +DELETE FROM posterminalcategory +WHERE posterminal_id = @posterminalId AND category_id = @categoryId +`; + +/** setOnAll: enable ALL company categories on one terminal. */ +export const SET_ON_ALL = ` +;WITH cats AS ( + SELECT id FROM category WHERE id = @rootCategoryId + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +) +INSERT INTO posterminalcategory (posterminal_id, category_id) +SELECT @posterminalId, cats.id +FROM cats +WHERE NOT EXISTS ( + SELECT 1 FROM posterminalcategory pc + WHERE pc.posterminal_id = @posterminalId AND pc.category_id = cats.id +) +`; + +/** setOffAll: remove ALL category links from one terminal. */ +export const SET_OFF_ALL = ` +DELETE FROM posterminalcategory WHERE posterminal_id = @posterminalId +`; + +/** For category tree: map category -> posterminal ids (company-scoped). */ +export const POSTERMINAL_IDS_BY_COMPANY = ` +SELECT + pc.category_id AS categoryId, + pc.posterminal_id AS posterminalId +FROM posterminalcategory pc +INNER JOIN posterminal pt ON pt.id = pc.posterminal_id +WHERE pt.company_id = @companyId +`; diff --git a/src/sql/purge.js b/src/sql/purge.js index 7c24353..54b2571 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -3,6 +3,8 @@ export const TABLES = [ 'companyuser', 'syncpos', 'companysetting', + 'posterminalcategory', + 'posterminal', 'numbers', 'manifestarticle', 'manifest', diff --git a/src/store/category/getTreeForCompany.js b/src/store/category/getTreeForCompany.js index 0a98f92..662de9f 100644 --- a/src/store/category/getTreeForCompany.js +++ b/src/store/category/getTreeForCompany.js @@ -1,10 +1,12 @@ import { GET_TREE_FOR_COMPANY } from '../../sql/category.js'; import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; import queryCategoryArticles from './queryCategoryArticles.js'; +import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js'; export default function getTreeForCompany(pool) { const access = resolveCompanyAccess(pool); const articles = queryCategoryArticles(pool); + const posterminalIds = getCategoryPosterminalIds(pool); return async (googleAuthId, companyId) => { const { rootCategoryId, languageId, posCustomerGroupId } = await access( @@ -12,18 +14,24 @@ export default function getTreeForCompany(pool) { companyId, ); - const [treeResult, articleRows] = await Promise.all([ + const [treeResult, articleRows, ptMap] = await Promise.all([ pool .request() .input('rootId', rootCategoryId) .input('languageId', languageId) .query(GET_TREE_FOR_COMPANY), articles({ categoryId: rootCategoryId, languageId, posCustomerGroupId }), + posterminalIds(companyId), ]); + const tree = treeResult.recordset.map((row) => ({ + ...row, + posterminalIds: ptMap[row.id] ?? [], + })); + return { languageId, - tree: treeResult.recordset, + tree, articles: articleRows, }; }; diff --git a/src/store/posterminal/delete.js b/src/store/posterminal/delete.js new file mode 100644 index 0000000..7fb538a --- /dev/null +++ b/src/store/posterminal/delete.js @@ -0,0 +1,24 @@ +import { DELETE_BY_COMPANY_AND_ID } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Posterminal not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .query(DELETE_BY_COMPANY_AND_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Posterminal not found'); + } + }; +} diff --git a/src/store/posterminal/getCategoryPosterminalIds.js b/src/store/posterminal/getCategoryPosterminalIds.js new file mode 100644 index 0000000..5e2d0b9 --- /dev/null +++ b/src/store/posterminal/getCategoryPosterminalIds.js @@ -0,0 +1,19 @@ +import { POSTERMINAL_IDS_BY_COMPANY } from '../../sql/posterminal.js'; + +export default function getCategoryPosterminalIds(pool) { + return async (companyId) => { + const result = await pool + .request() + .input('companyId', companyId) + .query(POSTERMINAL_IDS_BY_COMPANY); + + const map = {}; + for (const row of result.recordset) { + if (!map[row.categoryId]) { + map[row.categoryId] = []; + } + map[row.categoryId].push(row.posterminalId); + } + return map; + }; +} diff --git a/src/store/posterminal/list.js b/src/store/posterminal/list.js new file mode 100644 index 0000000..7d448e6 --- /dev/null +++ b/src/store/posterminal/list.js @@ -0,0 +1,17 @@ +import { LIST_BY_COMPANY } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId) => { + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .query(LIST_BY_COMPANY); + + return result.recordset; + }; +} diff --git a/src/store/posterminal/setOff.js b/src/store/posterminal/setOff.js new file mode 100644 index 0000000..5340e78 --- /dev/null +++ b/src/store/posterminal/setOff.js @@ -0,0 +1,39 @@ +import { GET_BY_ID, SET_OFF, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function setOff(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, posterminalId, categoryId) => { + if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { + throw new Error('Invalid posterminal or category'); + } + + await access(googleAuthId, companyId); + + const existing = await pool + .request() + .input('id', posterminalId) + .query(GET_BY_ID); + + if (!existing.recordset[0] || existing.recordset[0].companyId !== companyId) { + throw new Error('Posterminal not found'); + } + + await pool + .request() + .input('posterminalId', posterminalId) + .input('categoryId', categoryId) + .query(SET_OFF); + + const result = await pool + .request() + .input('posterminalId', posterminalId) + .query(LIST_CATEGORY_IDS); + + return { + id: posterminalId, + categoryIds: result.recordset.map((r) => r.categoryId), + }; + }; +} diff --git a/src/store/posterminal/setOffAll.js b/src/store/posterminal/setOffAll.js new file mode 100644 index 0000000..ec48b2e --- /dev/null +++ b/src/store/posterminal/setOffAll.js @@ -0,0 +1,39 @@ +import { GET_BY_ID, SET_OFF_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function setOffAll(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, posterminalId) => { + if (!Number.isFinite(posterminalId)) { + throw new Error('Invalid posterminal'); + } + + await access(googleAuthId, companyId); + + // Verify posterminal belongs to company + const pt = await pool + .request() + .input('id', posterminalId) + .query(GET_BY_ID); + + if (!pt.recordset[0] || pt.recordset[0].companyId !== companyId) { + throw new Error('Posterminal not found'); + } + + await pool + .request() + .input('posterminalId', posterminalId) + .query(SET_OFF_ALL); + + const result = await pool + .request() + .input('posterminalId', posterminalId) + .query(LIST_CATEGORY_IDS); + + return { + id: posterminalId, + categoryIds: result.recordset.map((r) => r.categoryId), + }; + }; +} diff --git a/src/store/posterminal/setOn.js b/src/store/posterminal/setOn.js new file mode 100644 index 0000000..74931e2 --- /dev/null +++ b/src/store/posterminal/setOn.js @@ -0,0 +1,39 @@ +import { GET_BY_ID, SET_ON, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function setOn(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, posterminalId, categoryId) => { + if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { + throw new Error('Invalid posterminal or category'); + } + + await access(googleAuthId, companyId); + + const existing = await pool + .request() + .input('id', posterminalId) + .query(GET_BY_ID); + + if (!existing.recordset[0] || existing.recordset[0].companyId !== companyId) { + throw new Error('Posterminal not found'); + } + + await pool + .request() + .input('posterminalId', posterminalId) + .input('categoryId', categoryId) + .query(SET_ON); + + const result = await pool + .request() + .input('posterminalId', posterminalId) + .query(LIST_CATEGORY_IDS); + + return { + id: posterminalId, + categoryIds: result.recordset.map((r) => r.categoryId), + }; + }; +} diff --git a/src/store/posterminal/setOnAll.js b/src/store/posterminal/setOnAll.js new file mode 100644 index 0000000..95409cd --- /dev/null +++ b/src/store/posterminal/setOnAll.js @@ -0,0 +1,52 @@ +import { GET_BY_ID, SET_ON_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import { GET_COMPANY_ROOT_CATEGORY } from '../../sql/company.js'; + +export default function setOnAll(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, posterminalId) => { + if (!Number.isFinite(posterminalId)) { + throw new Error('Invalid posterminal'); + } + + await access(googleAuthId, companyId); + + // Verify posterminal belongs to company + const pt = await pool + .request() + .input('id', posterminalId) + .query(GET_BY_ID); + + if (!pt.recordset[0] || pt.recordset[0].companyId !== companyId) { + throw new Error('Posterminal not found'); + } + + // Get the company's root category to walk the tree + const root = await pool + .request() + .input('companyId', companyId) + .query(GET_COMPANY_ROOT_CATEGORY); + + const rootCategoryId = root.recordset[0]?.rootCategoryId; + if (!rootCategoryId) { + throw new Error('Company not found'); + } + + await pool + .request() + .input('posterminalId', posterminalId) + .input('rootCategoryId', rootCategoryId) + .query(SET_ON_ALL); + + const result = await pool + .request() + .input('posterminalId', posterminalId) + .query(LIST_CATEGORY_IDS); + + return { + id: posterminalId, + categoryIds: result.recordset.map((r) => r.categoryId), + }; + }; +} diff --git a/src/store/posterminal/upsert.js b/src/store/posterminal/upsert.js new file mode 100644 index 0000000..eed06de --- /dev/null +++ b/src/store/posterminal/upsert.js @@ -0,0 +1,51 @@ +import { GET_BY_ID, CREATE, UPDATE } from '../../sql/posterminal.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function upsert(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, input = {}) => { + const pairingpin = input.pairingpin?.trim(); + if (!pairingpin) { + throw new Error('Invalid posterminal'); + } + const token = input.token ?? null; + const id = input.id ?? null; + + await access(googleAuthId, companyId); + + if (id) { + const existing = await pool + .request() + .input('id', id) + .query(GET_BY_ID); + + if (!existing.recordset[0] || existing.recordset[0].companyId !== companyId) { + throw new Error('Posterminal not found'); + } + + await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('token', token) + .input('pairingpin', pairingpin) + .query(UPDATE); + + return { id, token, pairingpin }; + } + + const created = await pool + .request() + .input('companyId', companyId) + .input('token', token) + .input('pairingpin', pairingpin) + .query(CREATE); + + return { + id: created.recordset[0].id, + token: created.recordset[0].token, + pairingpin: created.recordset[0].pairingpin, + }; + }; +}