diff --git a/README.md b/README.md index b8179b6..396d4bf 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,17 @@ For `PATCH /api/company/:id`: | Method | Signature | Returns | | --- | --- | --- | | `add` | `(email)` | `id` | -| `getByEmail` | `(email)` | `{ id, email, createdAt, selectedCompanyId } \| undefined` | -| `listAllowedEmails` | `()` | `{ id, email, createdAt, selectedCompanyId }[]` | +| `getByEmail` | `(email)` | `{ id, email, picture, sub, createdAt, selectedCompanyId } \| undefined` | +| `listAllowedEmails` | `()` | `{ id, email, picture, sub, createdAt, selectedCompanyId }[]` | | `removeAllowedEmail` | `(email)` | `void` | | `setSelectedCompany` | `(googleAuthId, companyId)` | `void` | +| `setProfile` | `(googleAuthId, { picture?: string \| null, sub?: string \| null })` | `void` | + +`picture` / `sub` come from the Google ID token (`payload.picture`, `payload.sub`). `sub` is Google’s stable subject id (unique when set). `setProfile` overwrites both columns (pass `null` to clear). Errors: `User not found`. Backend mapping: -- Login: `getByEmail(email)` +- Login: `getByEmail(email)` then `setProfile(id, { picture, sub })` from the verified token - CLI user manager: `listAllowedEmails()` / `removeAllowedEmail(email)` / `add(email)` - `PUT /api/auth/selected-company`: `setSelectedCompany(googleAuthId, companyId)` @@ -267,7 +270,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. | `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (see below) | | `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string })` | `{ id: number, parentId: number, name: string, sku: string }` | | `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) | -| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, date: Date, text: string }[]` | +| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, companyuserId: number \| null, date: Date, text: string }[]` | | `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string })` | `ArticleDetail` | | `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` | | `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` | @@ -381,9 +384,9 @@ type PriceEntry = { Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`. -`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. When `name` and/or unit (`unitamount`/`unitSi`) actually change, inserts one `articlehistory` row in the same transaction (`text` lines joined by newline), e.g. `Name geändert von "Alt" auf "Neu"` / `Einheit geändert von "500g" auf "1kg"` (label = `unitamount` + `unittext.symbol`, fallback `unit.si`). Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`. +`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. When `name` and/or unit (`unitamount`/`unitSi`) actually change, inserts one `articlehistory` row in the same transaction with `companyuser_id` set to the auth user (`text` lines joined by newline), e.g. `Name geändert von "Alt" auf "Neu"` / `Einheit geändert von "500g" auf "1kg"` (label = `unitamount` + `unittext.symbol`, fallback `unit.si`). Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`. -`getHistoryBySku` returns `articlehistory` for the article, newest first (`date` DESC, `id` DESC). Auth like `getBySku`. Errors: `Article not found`, `Not authorized`. +`getHistoryBySku` returns `articlehistory` for the article, newest first (`date` DESC, `id` DESC). `companyuserId` is null when the user was deleted (`ON DELETE SET NULL`). Auth like `getBySku`. Errors: `Article not found`, `Not authorized`. `setPricesBySku` replaces all `price` rows for the article’s company `poscustomergroup_id` in one transaction. Each entry: `price` → `net`, `tax` → tax rate (ensured via `tax.ensure`), `minmultiplier` defaults to `1`. Empty array clears all POS prices. Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `POS customer group required`, `Invalid prices`. diff --git a/schema-graph.dot b/schema-graph.dot index e7e16a6..61ab8a2 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -11,13 +11,13 @@ digraph schema { fontcolor="#1a365d"; margin=14; 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)
>]; + companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; 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)
>]; 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
>]; - companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; - google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
created_atDATETIME2
>]; + google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; @@ -32,14 +32,14 @@ digraph schema { label="Catalog"; labeljust="l"; fontsize=15; 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
>]; categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; 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
>]; + articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; optionslist [label=<
optionslist
idINT PK
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; } @@ -68,18 +68,18 @@ digraph schema { 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)
>]; + posterminal [label=<
posterminal
idINT PK
company_idINT FK
nameNVARCHAR(255)
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:w -> company:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; + google_auth:selected_company:sw -> 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: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:in5:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; + customergroup:company_id:nw -> company:in6: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:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_unit_company"]; @@ -88,39 +88,40 @@ digraph schema { brand:company_id:e -> company:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_brand_company"]; 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:articlelist_id:se -> articlelist:in1: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: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"]; - brandpic:pic_id:sw -> pic:in2:n [color="#c53030", fontcolor="#c53030", tooltip="FK_brandpic_pic"]; - 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: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: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: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: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"]; + articlehistory:article_id:sw -> article:in6:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlehistory_article"]; + articlehistory:companyuser_id:e -> companyuser:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articlehistory_companyuser"]; + articleean:article_id:se -> article:in4:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articleean_article"]; + articlepic:article_id:e -> article:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlepic_article"]; + articlepic:pic_id:sw -> pic:in1:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articlepic_pic"]; + brandpic:brand_id:e -> brand:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_brandpic_brand"]; + brandpic:pic_id:sw -> pic:in2:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_brandpic_pic"]; + articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlelistarticle_articlelist"]; + articlelistarticle:article_id:se -> article:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_articlelistarticle_article"]; + optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_optionslistarticle_optionslist"]; + optionslistarticle:article_id:sw -> article:in7:n [color="#c05621", fontcolor="#c05621", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in4:n [color="#319795", fontcolor="#319795", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:e -> article:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:se -> category:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categorypic_category"]; + categorypic:pic_id:se -> pic:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_categorypic_pic"]; + categorytext:category_id:e -> category:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in2:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorytext_language"]; + articletext:article_id:e -> article:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articletext_article"]; + articletext:language_id:sw -> language:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_articletext_language"]; + price:tax_id:sw -> tax:in3:n [color="#22543d", fontcolor="#22543d", tooltip="FK_price_tax"]; + price:article_id:w -> article:in8:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_price_article"]; + price:customergroup_id:sw -> customergroup:in2:n [color="#702459", fontcolor="#702459", tooltip="FK_price_customergroup"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:w -> article:in9:n [color="#285e61", fontcolor="#285e61", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:sw -> tax:in4:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:se -> company:in3:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:e -> google_auth:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:se -> company:in4:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_companyuser_company"]; + syncpos:company_id:se -> company:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_companysetting_company"]; + posterminal:company_id:e -> company:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_posterminal_company"]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_posterminalcategory_posterminal"]; + posterminalcategory:category_id:se -> category:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_posterminalcategory_category"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index f661f3e..765783f 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,233 +4,309 @@ - + 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 - - + + + + + + + +companyuser + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +companyuser:se->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 + + + +companyuser:e->google_auth: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 - - - + + + @@ -238,104 +314,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 - - + + @@ -343,8 +419,8 @@ company:e->tax:n - - + + @@ -352,450 +428,349 @@ 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:sw->company:n - - - - - - - - -google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -created_at -DATETIME2 - - - -companyuser:se->google_auth:n - - - + + + -google_auth:w->company:n +google_auth:sw->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:sw->language:n - - - + + + article - - - - - - - - - - - - -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 + + + + + + + + + + + + +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 - - - + + + - + categorytext:e->category:n - - - - - - - - -articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -articlehistory:e->article:n - - - + + + @@ -803,114 +778,176 @@ category:e->category:e - - + + - + categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK - + categoryarticle:sw->category:n - - - + + + - -categoryarticle:se->article:n - - - + +categoryarticle:e->article:n + + + + + + + + +articlelistarticle + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT + + + +articlelistarticle:se->article:n + + + + + + + + +articlelist + + + + +articlelist + + +id +INT +PK + + + +articlelistarticle:se->articlelist:n + + + brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) + + + + +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 - - - + + + @@ -918,8 +955,8 @@ article:sw->brand:n - - + + @@ -927,54 +964,40 @@ article:e->article:e - - + + - - -articlelist - - - - -articlelist - - -id -INT -PK - article:se->articlelist:n - - + + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK article:se->optionslist:n - - + + @@ -982,457 +1005,458 @@ article:e->unit:n - - + + - + -articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT +articlehistory + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - - -articlelistarticle:sw->article:n - - - + + +articlehistory:e->companyuser:n + + + - - -articlelistarticle:sw->articlelist:n - - - + + +articlehistory:sw->article: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:sw->article:n - - - + + + - + 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 - - - + + + - + 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 - - - + + + - + 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 - - - + + + - + price:w->article:n - - - + + + - + 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 - - - + + + - + manifestarticle:sw->tax:n - - - + + + - + manifestarticle:sw->manifest:n - - - + + + @@ -1440,113 +1464,117 @@ unit:e->company:n - - + + posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -posterminal_id -INT -FK - - -category_id -INT -FK + + +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) + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +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/scripts/test-api.js b/scripts/test-api.js index b0425e8..532f98f 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -271,6 +271,7 @@ const MENU = { { value: 'google_auth.listAllowedEmails', label: 'listAllowedEmails', hint: '→ rows' }, { value: 'google_auth.removeAllowedEmail', label: 'removeAllowedEmail', hint: 'email' }, { value: 'google_auth.setSelectedCompany', label: 'setSelectedCompany', hint: 'googleAuthId, companyId' }, + { value: 'google_auth.setProfile', label: 'setProfile', hint: 'googleAuthId, picture, sub' }, ], }, schema: { @@ -1125,6 +1126,19 @@ async function runMethod(client, action) { await client.google_auth.setSelectedCompany(Number(id), Number(companyId)); return ['ok', true]; } + case 'google_auth.setProfile': { + const id = await askInt('Auth id'); + if (id == null) return; + const picture = await askText('picture URL', { placeholder: '(empty = null)' }); + if (picture == null) return; + const sub = await askText('Google sub', { placeholder: '(empty = null)' }); + if (sub == null) return; + await client.google_auth.setProfile(Number(id), { + picture: picture.trim() || null, + sub: sub.trim() || null, + }); + return ['ok', true]; + } case 'initialize': { await client.initialize(); return ['ok', true]; diff --git a/src/index.js b/src/index.js index ac45f4f..a6cb526 100644 --- a/src/index.js +++ b/src/index.js @@ -61,6 +61,7 @@ import googleAuthGetByEmail from './store/google_auth/getByEmail.js'; import googleAuthListAllowedEmails from './store/google_auth/listAllowedEmails.js'; import googleAuthRemoveAllowedEmail from './store/google_auth/removeAllowedEmail.js'; import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js'; +import googleAuthSetProfile from './store/google_auth/setProfile.js'; import picGetImageByHash from './store/pic/getImageByHash.js'; import picGetThumbByHash from './store/pic/getThumbByHash.js'; import storePurge from './store/purge.js'; @@ -164,6 +165,7 @@ export default class LibDashStore { listAllowedEmails: googleAuthListAllowedEmails(pool), removeAllowedEmail: googleAuthRemoveAllowedEmail(pool), setSelectedCompany: googleAuthSetSelectedCompany(pool), + setProfile: googleAuthSetProfile(pool), }; this.pic = { diff --git a/src/schema.js b/src/schema.js index a8c0933..49fa134 100644 --- a/src/schema.js +++ b/src/schema.js @@ -21,11 +21,14 @@ BEGIN CREATE TABLE google_auth ( id INT IDENTITY(1,1) PRIMARY KEY, email NVARCHAR(320) NOT NULL, + picture NVARCHAR(2048) NULL, + sub NVARCHAR(255) NULL, created_at DATETIME2 NOT NULL CONSTRAINT DF_google_auth_created_at DEFAULT SYSUTCDATETIME(), selected_company INT NULL, CONSTRAINT UQ_google_auth_email UNIQUE (email), CONSTRAINT FK_google_auth_selected_company FOREIGN KEY (selected_company) REFERENCES company(id) ON DELETE SET NULL ); + CREATE UNIQUE INDEX UQ_google_auth_sub ON google_auth(sub) WHERE sub IS NOT NULL; END `; @@ -260,9 +263,11 @@ BEGIN CREATE TABLE articlehistory ( id INT IDENTITY(1,1) PRIMARY KEY, article_id INT NOT NULL, + companyuser_id INT NULL, [date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(), [text] NVARCHAR(MAX) NOT NULL, - CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE + CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_articlehistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL ); END `; @@ -615,7 +620,6 @@ export const SCHEMA_SCRIPTS = [ INIT_ARTICLELIST, INIT_BRAND, INIT_ARTICLE, - INIT_ARTICLEHISTORY, INIT_ARTICLEEAN, INIT_ARTICLEPIC, INIT_BRANDPIC, @@ -631,6 +635,7 @@ export const SCHEMA_SCRIPTS = [ INIT_NUMBERS, INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, + INIT_ARTICLEHISTORY, INIT_SYNCPOS, INIT_COMPANYSETTING, INIT_POSTERMINAL, diff --git a/src/sql/articlehistory.js b/src/sql/articlehistory.js index 0c2cd6a..4163ba3 100644 --- a/src/sql/articlehistory.js +++ b/src/sql/articlehistory.js @@ -1,10 +1,10 @@ export const INSERT = ` -INSERT INTO articlehistory (article_id, [text]) -VALUES (@articleId, @text) +INSERT INTO articlehistory (article_id, companyuser_id, [text]) +VALUES (@articleId, @companyuserId, @text) `; export const LIST_BY_ARTICLE_ID = ` -SELECT id, [date], [text] +SELECT id, companyuser_id AS companyuserId, [date], [text] FROM articlehistory WHERE article_id = @articleId ORDER BY [date] DESC, id DESC diff --git a/src/sql/google_auth.js b/src/sql/google_auth.js index 3c5e215..089e2da 100644 --- a/src/sql/google_auth.js +++ b/src/sql/google_auth.js @@ -5,13 +5,25 @@ VALUES (@email) `; export const GET_BY_EMAIL = ` -SELECT id, email, created_at AS createdAt, selected_company AS selectedCompanyId +SELECT + id, + email, + picture, + sub, + created_at AS createdAt, + selected_company AS selectedCompanyId FROM google_auth WHERE email = @email `; export const LIST_ALLOWED_EMAILS = ` -SELECT id, email, created_at AS createdAt, selected_company AS selectedCompanyId +SELECT + id, + email, + picture, + sub, + created_at AS createdAt, + selected_company AS selectedCompanyId FROM google_auth ORDER BY email `; @@ -26,3 +38,9 @@ UPDATE google_auth SET selected_company = @companyId WHERE id = @googleAuthId `; + +export const SET_PROFILE = ` +UPDATE google_auth +SET picture = @picture, sub = @sub +WHERE id = @googleAuthId +`; diff --git a/src/store/article/getHistoryBySku.js b/src/store/article/getHistoryBySku.js index 190df71..6f7ac8d 100644 --- a/src/store/article/getHistoryBySku.js +++ b/src/store/article/getHistoryBySku.js @@ -14,6 +14,7 @@ export default function getHistoryBySku(pool) { return result.recordset.map((row) => ({ id: row.id, + companyuserId: row.companyuserId ?? null, date: row.date, text: row.text, })); diff --git a/src/store/article/resolveArticleBySku.js b/src/store/article/resolveArticleBySku.js index 3c8effc..e30ab3d 100644 --- a/src/store/article/resolveArticleBySku.js +++ b/src/store/article/resolveArticleBySku.js @@ -26,6 +26,7 @@ export default function resolveArticleBySku(pool) { companyId: row.companyId, rootCategoryId: row.rootCategoryId, posCustomerGroupId: row.posCustomerGroupId ?? null, + companyuserId: row.companyuserId, }; }; } diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index 7f8de51..63f300d 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -41,7 +41,10 @@ export default function updateBySku(pool) { throw new Error('Invalid unitamount'); } - const { articleId, languageId, companyId } = await resolve(googleAuthId, sku); + const { articleId, languageId, companyId, companyuserId } = await resolve( + googleAuthId, + sku, + ); const currentResult = await pool .request() @@ -134,6 +137,7 @@ export default function updateBySku(pool) { if (historyLines.length) { await request(pool, tx) .input('articleId', articleId) + .input('companyuserId', companyuserId) .input('text', historyLines.join('\n')) .query(INSERT_HISTORY); } diff --git a/src/store/google_auth/setProfile.js b/src/store/google_auth/setProfile.js new file mode 100644 index 0000000..4c1593a --- /dev/null +++ b/src/store/google_auth/setProfile.js @@ -0,0 +1,16 @@ +import { SET_PROFILE } from '../../sql/google_auth.js'; + +export default function setProfile(pool) { + return async (googleAuthId, { picture = null, sub = null } = {}) => { + const result = await pool + .request() + .input('googleAuthId', googleAuthId) + .input('picture', picture) + .input('sub', sub) + .query(SET_PROFILE); + + if (!result.rowsAffected[0]) { + throw new Error('User not found'); + } + }; +}