diff --git a/README.md b/README.md index c3daa7f..e1c4799 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,50 @@ Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` ( --- +## `delivery` + +Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type = 'delivery'`. Auth via `resolveCompanyAccess`. `manifest.company_id` scopes the list (`ON DELETE NO ACTION` — avoid SQL Server multiple cascade paths with `entity_id`). + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `DeliverySummary[]` | +| `getById` | `(googleAuthId, companyId, id)` | `Delivery` | +| `create` | `(googleAuthId, companyId, { number, date, supplierId?, lines })` | `Delivery` | +| `update` | `(googleAuthId, companyId, id, { number?, date?, supplierId?, lines? })` | `Delivery` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | + +```ts +type DeliverySummary = { + id: number, + number: string, + type: 'delivery', + supplierId: number | null, // manifest.entity_id + date: Date, +} + +type DeliveryLine = { + id: number, + articleId: number | null, + sku: string | null, + amount: number, + net: number, + transactedAmount: number, + rebate: number, + tax: number, // tax.rate + comment: string | null, +} + +type Delivery = DeliverySummary & { lines: DeliveryLine[] } +``` + +`create` inserts header + lines in one transaction. `supplierId` null or a Lieferanten `entity` of the company (else `Supplier not found`). Each line requires `sku` (resolved in the company category tree → `Article not found`), `amount`, `net`, `tax` (rate → `tax.ensure`); `rebate` defaults `0`; `transactedAmount` defaults to `amount`. Empty/invalid `number` or `date` → `Invalid delivery`; bad line payload → `Invalid lines`. + +`update` patches header fields that are present. If `lines` is present, replace-all lines; if omitted, leave lines unchanged. Returns full `Delivery`. + +`delete` removes the manifest (`manifestarticle` cascades). Errors: `Company not found`, `Not authorized`, `Delivery not found`, `Supplier not found`, `Article not found`, `Invalid delivery`, `Invalid lines`. + +--- + ## `posterminal` Company-scoped POS terminals. Auth via `resolveCompanyAccess`. @@ -479,6 +523,11 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `POST /api/supplier` | `supplier.create(googleAuthId, companyId, { name })` | | `PATCH /api/supplier/:id` | `supplier.update(googleAuthId, companyId, id, { name })` | | `DELETE /api/supplier/:id` | `supplier.delete(googleAuthId, companyId, id)` | +| `GET /api/delivery` | `delivery.list(googleAuthId, companyId)` | +| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, companyId, id)` | +| `POST /api/delivery` | `delivery.create(googleAuthId, companyId, { number, date, supplierId?, lines })` | +| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, companyId, id, { number?, date?, supplierId?, lines? })` | +| `DELETE /api/delivery/:id` | `delivery.delete(googleAuthId, companyId, id)` | | `GET /api/posterminal` | `posterminal.list(googleAuthId, companyId)` | | `PUT /api/posterminal` | `posterminal.upsert(googleAuthId, companyId, { id?, name, token?, pairingpin })` | | `DELETE /api/posterminal/:id` | `posterminal.delete(googleAuthId, companyId, id)` | diff --git a/schema-graph.dot b/schema-graph.dot index 4221cc3..5797eb8 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -9,58 +9,58 @@ digraph schema { label="Identity & Company"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; - meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; - numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; + numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; + 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)
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; - company [label=<
company
idINT PK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
poscustomergroup_idINT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; + meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - 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
>]; + companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - unittext [label=<
unittext
idINT PK
unit_idINT FK
language_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; + unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; - articletext [label=<
articletext
idINT PK
article_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; - categorytext [label=<
categorytext
idINT PK
category_idINT FK
language_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)
>]; + articletext [label=<
articletext
idINT PK
language_idINT FK
article_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="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - 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
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; articlelist [label=<
articlelist
idINT PK
>]; article [label=<
article
idINT PK
unit_idINT FK
articlelist_id?INT FK
fatherarticle_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; - articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - optionslist [label=<
optionslist
idINT PK
>]; articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + optionslist [label=<
optionslist
idINT PK
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; + articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + 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
article_idINT FK
category_idINT FK
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; - brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; } subgraph cluster_commerce { label="Pricing & Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - price [label=<
price
idINT PK
tax_idINT FK
customergroup_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; - manifest [label=<
manifest
idINT PK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; - manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
tax_idINT FK
article_id?INT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; + manifest [label=<
manifest
idINT PK
company_idINT FK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; + manifestarticle [label=<
manifestarticle
idINT PK
article_id?INT FK
tax_idINT FK
manifest_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; + price [label=<
price
idINT PK
article_idINT FK
customergroup_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -68,64 +68,65 @@ 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
>]; + posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; 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:sw -> company:in5: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:e -> category:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; - company:default_language_id:e -> language:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; - company:taxrate:w -> tax:in4:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate"]; - company:reducedtaxrate:w -> tax:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; - company:taxfreerate:w -> tax:in3:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; - customergroup:company_id:ne -> company:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; - company:poscustomergroup_id:sw -> customergroup:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; - entity:customergroup_id:se -> customergroup:in1:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; - entityhistory:entity_id:sw -> entity:in1:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityhistory_entity"]; - entityhistory:companyuser_id:se -> companyuser:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entityhistory_companyuser"]; - unit:company_id:w -> company:in7:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unit_company"]; - unittext:unit_id:w -> unit:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_unittext_unit"]; - unittext:language_id:se -> language:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unittext_language"]; - brand:company_id:w -> company:in8:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_brand_company"]; - article:unit_id:w -> unit:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_article_unit"]; - article:brand_id:se -> brand:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_brand"]; + 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:se -> company:in3:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; + company:poscustomergroup_id:nw -> customergroup:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; + entity:customergroup_id:sw -> customergroup:in1:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; + entityhistory:entity_id:sw -> entity:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityhistory_entity"]; + entityhistory:companyuser_id:se -> companyuser:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entityhistory_companyuser"]; + unit:company_id:e -> company:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unit_company"]; + unittext:unit_id:se -> unit:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_unittext_unit"]; + unittext:language_id:se -> language:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unittext_language"]; + brand:company_id:e -> company:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_brand_company"]; + article:unit_id:sw -> unit:in1:n [color="#702459", fontcolor="#702459", tooltip="FK_article_unit"]; + article:brand_id:e -> brand:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_brand"]; article:articlelist_id:sw -> articlelist:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_articlelist"]; article:optionslist_id:se -> optionslist:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_optionslist"]; article:fatherarticle_id:e -> article:id:e [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:sw -> article:in5:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlehistory_article"]; - articlehistory:companyuser_id:w -> companyuser:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlehistory_companyuser"]; - articleean:article_id:sw -> article:in6:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articleean_article"]; - articlepic:article_id:w -> article:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlepic_article"]; + articlehistory:article_id:w -> article:in5:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlehistory_article"]; + articlehistory:companyuser_id:e -> companyuser:in0:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlehistory_companyuser"]; + articleean:article_id:sw -> article:in3:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articleean_article"]; + articlepic:article_id:w -> article:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlepic_article"]; articlepic:pic_id:se -> pic:in1:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlepic_pic"]; brandpic:brand_id:w -> brand:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_brandpic_brand"]; - brandpic:pic_id:sw -> pic:in2:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_brandpic_pic"]; + brandpic:pic_id:se -> pic:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_brandpic_pic"]; articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_articlelistarticle_articlelist"]; - articlelistarticle:article_id:se -> article:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_articlelistarticle_article"]; + articlelistarticle:article_id:se -> article:in1:n [color="#c05621", fontcolor="#c05621", tooltip="FK_articlelistarticle_article"]; optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:w -> article:in7:n [color="#97266d", fontcolor="#97266d", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:sw -> category:in3:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:se -> article:in2:n [color="#276749", fontcolor="#276749", tooltip="FK_categoryarticle_article"]; + optionslistarticle:article_id:sw -> article:in4:n [color="#97266d", fontcolor="#97266d", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:w -> article:in6:n [color="#276749", fontcolor="#276749", tooltip="FK_categoryarticle_article"]; categorypic:category_id:sw -> category:in4:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorypic_category"]; - categorypic:pic_id:se -> pic:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorypic_pic"]; - categorytext:category_id:w -> category:in5:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorytext_category"]; - categorytext:language_id:sw -> language:in3:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorytext_language"]; - articletext:article_id:w -> article:in9:n [color="#22543d", fontcolor="#22543d", tooltip="FK_articletext_article"]; + categorypic:pic_id:sw -> pic:in2:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorypic_pic"]; + categorytext:category_id:e -> category:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorytext_language"]; + articletext:article_id:e -> article:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_articletext_article"]; articletext:language_id:sw -> language:in2:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_articletext_language"]; - price:tax_id:se -> tax:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_price_tax"]; - price:article_id:e -> article:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_price_article"]; - price:customergroup_id:se -> customergroup:in0:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_customergroup"]; - manifest:entity_id:se -> entity:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifest_entity"]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_manifestarticle_manifest"]; - manifestarticle:article_id:e -> article:in1:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifestarticle_article"]; - manifestarticle:tax_id:se -> tax:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_numbers_company"]; - companyuser:google_auth_id:se -> google_auth:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:sw -> company:in4:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_companyuser_company"]; - syncpos:company_id:se -> company:in2:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_syncpos_company"]; - companysetting:company_id:sw -> company:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_companysetting_company"]; - posterminal:company_id:w -> company:in6:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_posterminal_company"]; - posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_posterminalcategory_posterminal"]; - posterminalcategory:category_id:se -> category:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_posterminalcategory_category"]; + price:tax_id:sw -> tax:in4:n [color="#702459", fontcolor="#702459", tooltip="FK_price_tax"]; + price:article_id:w -> article:in9:n [color="#744210", fontcolor="#744210", tooltip="FK_price_article"]; + price:customergroup_id:w -> customergroup:in2:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_customergroup"]; + manifest:company_id:sw -> company:in9:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifest_company"]; + manifest:entity_id:w -> entity:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_manifest_entity"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:w -> article:in8:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:sw -> tax:in3:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:se -> company:in4:n [color="#c53030", fontcolor="#c53030", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:se -> google_auth:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:sw -> company:in6:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_companyuser_company"]; + syncpos:company_id:sw -> company:in5:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_companysetting_company"]; + posterminal:company_id:e -> company:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_posterminal_company"]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_posterminalcategory_posterminal"]; + posterminalcategory:category_id:sw -> category:in3:n [color="#97266d", fontcolor="#97266d", tooltip="FK_posterminalcategory_category"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index d1fa67f..2360bcd 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,939 +4,891 @@ - + 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 - - - -entity - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + +Units - + customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) - - - -entity:se->customergroup:n - - - - - - - - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - - -numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) - + company - - - - - - - - - - - -company - - -id -INT -PK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -poscustomergroup_id -INT -FK - - -root_category_id -? -INT -FK - - -default_language_id -? -INT -FK - - -name -NVARCHAR(255) - - - -numbers:se->company:n - - - - - + + + + + + + + + + + + +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) -customergroup:ne->company:n +customergroup:se->company:n - - + + - - -syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) + + +numbers + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - - -syncpos:se->company:n - - - + + +numbers:se->company:n + + + -company:sw->customergroup:n +company:nw->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) -company:e->language:n +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:e->category:n +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 +company:e->tax:n - - + + -company:w->tax:n +company:e->tax:n - - + + -company:w->tax:n +company:e->tax:n - - + + + + +entity + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +entity:sw->customergroup:n + + + + + + + + +syncpos + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) + + + +syncpos:sw->company:n + + + + + + + + +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + 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:sw->entity: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 entityhistory:se->companyuser:n - - - - - - - -companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - - -companysetting:sw->company:n - - - + + - + companyuser:sw->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:se->google_auth:n - - - + + + + + + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + + +companysetting:sw->company:n + + + -google_auth:sw->company:n +google_auth:w->company:n - - + + unittext - - -unittext - - -id -INT -PK - - -unit_id -INT -FK - - -language_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:se->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:w->unit:n +unittext:se->unit: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:sw->language:n - - - - - - - - -article - - - - - - - - - - - - -article - - -id -INT -PK - - -unit_id -INT -FK - - -articlelist_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 - - - -articletext:w->article: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 + + +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:w->category:n +categorytext:e->category:n - - + + - - -category:e->category:e - - - + + +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:sw->language:n + + + - - -categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK + + +article + + + + + + + + + + + + +article + + +id +INT +PK + + +unit_id +INT +FK + + +articlelist_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +brand_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 - - -categoryarticle:sw->category:n - - - - - - - - -categoryarticle:se->article:n - - - + + +articletext:e->article:n + + + - + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -articlelist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT - + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK articlelistarticle:se->articlelist:n - - + + @@ -944,8 +896,8 @@ articlelistarticle:se->article:n - - + + @@ -953,8 +905,8 @@ article:sw->articlelist:n - - + + @@ -962,207 +914,149 @@ article:e->article:e - - + + - + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK article:se->optionslist: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) -article:se->brand:n +article:e->brand:n - - + + -article:w->unit:n +article:sw->unit:n - - - - - - - -articlehistory - - -articlehistory - - -id -INT -PK - - -companyuser_id -? -INT -FK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -articlehistory:w->companyuser:n - - - - - - - - -articlehistory:sw->article: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:sw->article:n - - - - - - - -brand:w->company: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 - - + + @@ -1170,142 +1064,249 @@ optionslistarticle:sw->optionslist:n - - + + - - -categorypic - - -categorypic - - -id -INT -PK - - -category_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlehistory + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - - -categorypic:sw->category:n - - - + + +articlehistory:e->companyuser:n + + + + + + + + +articlehistory:w->article:n + + + + + + + + +brand:e->company:n + + + + + + + + +category:e->category:e + + + + + + + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK + + + +categoryarticle:w->article:n + + + + + + + + +categoryarticle:sw->category:n + + + + + + + + +brandpic + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +brandpic:w->brand: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 - - - + + +brandpic:se->pic:n + + + articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT articlepic:w->article:n - - + + @@ -1313,347 +1314,361 @@ articlepic:se->pic:n - - + + - + -brandpic - - -brandpic - - -id -INT -PK - - -brand_id -INT -FK - - -pic_id -INT -FK - - -sort -INT +categorypic + + +categorypic + + +id +INT +PK + + +category_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - - -brandpic:w->brand:n - - - + + +categorypic:sw->category:n + + + - - -brandpic:sw->pic:n - - - - - - - - -price - - -price - - -id -INT -PK - - -tax_id -INT -FK - - -customergroup_id -INT -FK - - -article_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT - - - -price:se->customergroup:n - - - - - - - - -price:e->article:n - - - - - - - - -price:se->tax:n - - - + + +categorypic:sw->pic:n + + + manifest - - - -manifest - - -id -INT -PK - - -entity_id -? -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 + + + +manifest + + +id +INT +PK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 + + + +manifest:sw->company:n + + + + + - -manifest:se->entity:n - - - + +manifest:w->entity:n + + + manifestarticle - - -manifestarticle - - -id -INT -PK - - -manifest_id -INT -FK - - -tax_id -INT -FK - - -article_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:e->article:n - - - - - - - - -manifestarticle:sw->manifest:n - - - + +manifestarticle:w->article:n + + + - -manifestarticle:se->tax:n - - - + +manifestarticle:sw->tax:n + + + + + + + + +manifestarticle:sw->manifest: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:w->customergroup:n + + + + + + + + +price:w->article:n + + + + + + + + +price:sw->tax:n + + + -unit:w->company:n +unit:e->company: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:se->category:n - - - + +posterminalcategory:sw->category:n + + + posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -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:w->company: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 7993ed2..053200b 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -183,6 +183,36 @@ const MENU = { }, ], }, + delivery: { + label: 'delivery', + items: [ + { + value: 'delivery.list', + label: 'list', + hint: 'googleAuthId, companyId → summaries', + }, + { + value: 'delivery.getById', + label: 'getById', + hint: 'googleAuthId, companyId, id → delivery', + }, + { + value: 'delivery.create', + label: 'create', + hint: 'googleAuthId, companyId, JSON body → delivery', + }, + { + value: 'delivery.update', + label: 'update', + hint: 'googleAuthId, companyId, id, JSON patch → delivery', + }, + { + value: 'delivery.delete', + label: 'delete', + hint: 'googleAuthId, companyId, id', + }, + ], + }, category: { label: 'category', items: [ @@ -712,6 +742,108 @@ async function runMethod(client, action) { ); return ['ok', true]; } + case 'delivery.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + return [ + 'result[]', + await client.delivery.list(Number(googleAuthId), Number(companyId)), + ]; + } + case 'delivery.getById': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('delivery id'); + if (id == null) return; + return [ + 'result', + await client.delivery.getById( + Number(googleAuthId), + Number(companyId), + Number(id), + ), + ]; + } + case 'delivery.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const bodyJson = await askText('delivery JSON', { + placeholder: + '{"number":"DHL-123","date":"2026-08-10","supplierId":1,"lines":[{"sku":"1","amount":10,"net":99.5,"tax":19}]}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, + }); + if (bodyJson == null) return; + return [ + 'result', + await client.delivery.create( + Number(googleAuthId), + Number(companyId), + JSON.parse(bodyJson), + ), + ]; + } + case 'delivery.update': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('delivery id'); + if (id == null) return; + const patchJson = await askText('patch JSON', { + placeholder: '{"number":"DHL-124","lines":[]}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, + }); + if (patchJson == null) return; + return [ + 'result', + await client.delivery.update( + Number(googleAuthId), + Number(companyId), + Number(id), + JSON.parse(patchJson), + ), + ]; + } + case 'delivery.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('delivery id'); + if (id == null) return; + await client.delivery.delete( + Number(googleAuthId), + Number(companyId), + Number(id), + ); + return ['ok', true]; + } case 'lang.create': { const iso639 = await askText('ISO 639', { placeholder: 'de', diff --git a/src/index.js b/src/index.js index 92d48ed..17a2a95 100644 --- a/src/index.js +++ b/src/index.js @@ -43,6 +43,11 @@ import supplierList from './store/supplier/list.js'; import supplierCreate from './store/supplier/create.js'; import supplierUpdate from './store/supplier/update.js'; import supplierDelete from './store/supplier/delete.js'; +import deliveryList from './store/delivery/list.js'; +import deliveryGetById from './store/delivery/getById.js'; +import deliveryCreate from './store/delivery/create.js'; +import deliveryUpdate from './store/delivery/update.js'; +import deliveryDelete from './store/delivery/delete.js'; import posterminalList from './store/posterminal/list.js'; import posterminalUpsert from './store/posterminal/upsert.js'; import posterminalDelete from './store/posterminal/delete.js'; @@ -160,6 +165,14 @@ export default class LibDashStore { delete: supplierDelete(pool), }; + this.delivery = { + list: deliveryList(pool), + getById: deliveryGetById(pool), + create: deliveryCreate(pool), + update: deliveryUpdate(pool), + delete: deliveryDelete(pool), + }; + this.posterminal = { list: posterminalList(pool), upsert: posterminalUpsert(pool), diff --git a/src/schema.js b/src/schema.js index c83ff48..bbf1e00 100644 --- a/src/schema.js +++ b/src/schema.js @@ -468,10 +468,12 @@ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifest') BEGIN CREATE TABLE manifest ( id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, [number] NVARCHAR(100) NOT NULL, [type] NVARCHAR(100) NOT NULL, entity_id INT NULL, [date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME(), + CONSTRAINT FK_manifest_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION, CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL ); END diff --git a/src/sql/delivery.js b/src/sql/delivery.js new file mode 100644 index 0000000..1cdefe4 --- /dev/null +++ b/src/sql/delivery.js @@ -0,0 +1,122 @@ +export const DELIVERY_TYPE = 'delivery'; + +export const LIST_DELIVERIES = ` +SELECT + m.id, + m.[number], + m.[type], + m.entity_id AS supplierId, + m.[date] +FROM manifest m +WHERE m.company_id = @companyId + AND m.[type] = @type +ORDER BY m.[date] DESC, m.id DESC +`; + +export const GET_DELIVERY_HEADER = ` +SELECT + m.id, + m.[number], + m.[type], + m.entity_id AS supplierId, + m.[date] +FROM manifest m +WHERE m.id = @id + AND m.company_id = @companyId + AND m.[type] = @type +`; + +export const GET_DELIVERY_LINES = ` +SELECT + ma.id, + ma.article_id AS articleId, + a.sku, + ma.amount, + ma.net, + ma.transactedamount AS transactedAmount, + ma.rebate, + t.rate AS tax, + ma.comment +FROM manifestarticle ma +LEFT JOIN article a ON a.id = ma.article_id +INNER JOIN tax t ON t.id = ma.tax_id +WHERE ma.manifest_id = @manifestId +ORDER BY ma.id +`; + +export const CREATE_MANIFEST = ` +INSERT INTO manifest (company_id, [number], [type], entity_id, [date]) +OUTPUT INSERTED.id +VALUES (@companyId, @number, @type, @entityId, @date) +`; + +export const UPDATE_MANIFEST = ` +UPDATE manifest +SET + [number] = @number, + entity_id = @entityId, + [date] = @date +WHERE id = @id + AND company_id = @companyId + AND [type] = @type +`; + +export const DELETE_MANIFEST = ` +DELETE FROM manifest +WHERE id = @id + AND company_id = @companyId + AND [type] = @type +`; + +export const DELETE_LINES_BY_MANIFEST = ` +DELETE FROM manifestarticle +WHERE manifest_id = @manifestId +`; + +export const CREATE_LINE = ` +INSERT INTO manifestarticle ( + manifest_id, + article_id, + amount, + net, + transactedamount, + comment, + rebate, + tax_id +) +OUTPUT INSERTED.id +VALUES ( + @manifestId, + @articleId, + @amount, + @net, + @transactedAmount, + @comment, + @rebate, + @taxId +) +`; + +/** Resolve SKU to article id if reachable via the company category tree. */ +export const RESOLVE_ARTICLE_ID_BY_SKU = ` +;WITH category_tree AS ( + SELECT id + FROM category + WHERE id = @rootCategoryId + + UNION ALL + + SELECT c.id + FROM category c + INNER JOIN category_tree ct ON c.parent_id = ct.id +) +SELECT TOP 1 a.id +FROM article a +WHERE a.sku = @sku + AND EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN category_tree ct ON ct.id = ca.category_id + WHERE ca.article_id = a.id +) +`; diff --git a/src/store/delivery/create.js b/src/store/delivery/create.js new file mode 100644 index 0000000..704b869 --- /dev/null +++ b/src/store/delivery/create.js @@ -0,0 +1,67 @@ +import { CREATE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { + loadDelivery, + normalizeLines, + parseDeliveryDate, +} from './helpers.js'; +import { assertSupplier, insertLines } from './writeLines.js'; + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + return async (googleAuthId, companyId, body = {}) => { + const number = body.number?.trim?.() ? body.number.trim() : null; + const date = parseDeliveryDate(body.date); + if (!number || !date) { + throw new Error('Invalid delivery'); + } + + const lines = normalizeLines(body.lines ?? []); + + const { rootCategoryId } = await access(googleAuthId, companyId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const supplierId = await assertSupplier( + pool, + tx, + companyId, + body.supplierId ?? null, + ); + + const created = await request(pool, tx) + .input('companyId', companyId) + .input('number', number) + .input('type', DELIVERY_TYPE) + .input('entityId', supplierId) + .input('date', date) + .query(CREATE_MANIFEST); + + const manifestId = created.recordset[0].id; + + await insertLines(pool, tx, { + manifestId, + rootCategoryId, + lines, + ensureTax, + }); + + const delivery = await loadDelivery(pool, tx, companyId, manifestId); + await tx.commit(); + return delivery; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/delivery/delete.js b/src/store/delivery/delete.js new file mode 100644 index 0000000..eb900a9 --- /dev/null +++ b/src/store/delivery/delete.js @@ -0,0 +1,25 @@ +import { DELETE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.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('Delivery not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('type', DELIVERY_TYPE) + .query(DELETE_MANIFEST); + + if (!result.rowsAffected[0]) { + throw new Error('Delivery not found'); + } + }; +} diff --git a/src/store/delivery/getById.js b/src/store/delivery/getById.js new file mode 100644 index 0000000..eb6daad --- /dev/null +++ b/src/store/delivery/getById.js @@ -0,0 +1,20 @@ +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import { loadDelivery } from './helpers.js'; + +export default function getById(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Delivery not found'); + } + + await access(googleAuthId, companyId); + + const delivery = await loadDelivery(pool, null, companyId, id); + if (!delivery) { + throw new Error('Delivery not found'); + } + return delivery; + }; +} diff --git a/src/store/delivery/helpers.js b/src/store/delivery/helpers.js new file mode 100644 index 0000000..a644edf --- /dev/null +++ b/src/store/delivery/helpers.js @@ -0,0 +1,104 @@ +import { + DELIVERY_TYPE, + GET_DELIVERY_HEADER, + GET_DELIVERY_LINES, +} from '../../sql/delivery.js'; +import request from '../request.js'; + +export function shapeSummary(row) { + return { + id: row.id, + number: row.number, + type: DELIVERY_TYPE, + supplierId: row.supplierId ?? null, + date: row.date, + }; +} + +export function shapeLine(row) { + return { + id: row.id, + articleId: row.articleId ?? null, + sku: row.sku ?? null, + amount: row.amount, + net: row.net, + transactedAmount: row.transactedAmount, + rebate: row.rebate, + tax: row.tax, + comment: row.comment ?? null, + }; +} + +export async function loadDelivery(pool, tx, companyId, id) { + const header = await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', DELIVERY_TYPE) + .query(GET_DELIVERY_HEADER); + + const row = header.recordset[0]; + if (!row) { + return null; + } + + const lines = await request(pool, tx) + .input('manifestId', id) + .query(GET_DELIVERY_LINES); + + return { + ...shapeSummary(row), + lines: lines.recordset.map(shapeLine), + }; +} + +export function parseDeliveryDate(value) { + if (value instanceof Date && !Number.isNaN(value.getTime())) { + return value; + } + if (typeof value === 'string' && value.trim()) { + const d = new Date(value); + if (!Number.isNaN(d.getTime())) { + return d; + } + } + return null; +} + +export function normalizeLines(lines) { + if (!Array.isArray(lines)) { + throw new Error('Invalid lines'); + } + + return lines.map((entry) => { + const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null; + if (!sku) { + throw new Error('Invalid lines'); + } + + const amount = entry?.amount; + const net = entry?.net; + const rebate = + entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; + const tax = entry?.tax; + const transactedAmount = + entry?.transactedAmount === undefined || entry?.transactedAmount === null + ? amount + : entry.transactedAmount; + const comment = + entry?.comment === undefined || entry?.comment === null + ? null + : String(entry.comment); + + if ( + !Number.isFinite(amount) || + !Number.isFinite(net) || + !Number.isFinite(rebate) || + !Number.isFinite(tax) || + !Number.isFinite(transactedAmount) + ) { + throw new Error('Invalid lines'); + } + + return { sku, amount, net, rebate, tax, transactedAmount, comment }; + }); +} diff --git a/src/store/delivery/list.js b/src/store/delivery/list.js new file mode 100644 index 0000000..b95a931 --- /dev/null +++ b/src/store/delivery/list.js @@ -0,0 +1,19 @@ +import { DELIVERY_TYPE, LIST_DELIVERIES } from '../../sql/delivery.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import { shapeSummary } from './helpers.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) + .input('type', DELIVERY_TYPE) + .query(LIST_DELIVERIES); + + return result.recordset.map(shapeSummary); + }; +} diff --git a/src/store/delivery/update.js b/src/store/delivery/update.js new file mode 100644 index 0000000..ed55901 --- /dev/null +++ b/src/store/delivery/update.js @@ -0,0 +1,123 @@ +import { + DELETE_LINES_BY_MANIFEST, + DELIVERY_TYPE, + GET_DELIVERY_HEADER, + UPDATE_MANIFEST, +} from '../../sql/delivery.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { + loadDelivery, + normalizeLines, + parseDeliveryDate, + shapeSummary, +} from './helpers.js'; +import { assertSupplier, insertLines } from './writeLines.js'; + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + return async (googleAuthId, companyId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Delivery not found'); + } + + const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number'); + const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date'); + const hasSupplierId = Object.prototype.hasOwnProperty.call( + patch, + 'supplierId', + ); + const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines'); + + if (!hasNumber && !hasDate && !hasSupplierId && !hasLines) { + throw new Error('Invalid delivery'); + } + + let normalizedLines; + if (hasLines) { + normalizedLines = normalizeLines(patch.lines); + } + + const { rootCategoryId } = await access(googleAuthId, companyId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const existing = await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', DELIVERY_TYPE) + .query(GET_DELIVERY_HEADER); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Delivery not found'); + } + + const summary = shapeSummary(current); + + let number = summary.number; + if (hasNumber) { + number = patch.number?.trim?.() ? patch.number.trim() : null; + if (!number) { + throw new Error('Invalid delivery'); + } + } + + let date = summary.date; + if (hasDate) { + date = parseDeliveryDate(patch.date); + if (!date) { + throw new Error('Invalid delivery'); + } + } + + let supplierId = summary.supplierId; + if (hasSupplierId) { + supplierId = await assertSupplier( + pool, + tx, + companyId, + patch.supplierId ?? null, + ); + } + + await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', DELIVERY_TYPE) + .input('number', number) + .input('entityId', supplierId) + .input('date', date) + .query(UPDATE_MANIFEST); + + if (hasLines) { + await request(pool, tx) + .input('manifestId', id) + .query(DELETE_LINES_BY_MANIFEST); + + await insertLines(pool, tx, { + manifestId: id, + rootCategoryId, + lines: normalizedLines, + ensureTax, + }); + } + + const delivery = await loadDelivery(pool, tx, companyId, id); + await tx.commit(); + return delivery; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/delivery/writeLines.js b/src/store/delivery/writeLines.js new file mode 100644 index 0000000..34d3929 --- /dev/null +++ b/src/store/delivery/writeLines.js @@ -0,0 +1,66 @@ +import { + CREATE_LINE, + RESOLVE_ARTICLE_ID_BY_SKU, +} from '../../sql/delivery.js'; +import { GET_SUPPLIER_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; +import request from '../request.js'; + +export async function assertSupplier(pool, tx, companyId, supplierId) { + if (supplierId == null) { + return null; + } + if (!Number.isFinite(supplierId)) { + throw new Error('Supplier not found'); + } + + const result = await request(pool, tx) + .input('id', supplierId) + .input('companyId', companyId) + .input('groupName', SUPPLIER_GROUP_NAME) + .query(GET_SUPPLIER_BY_ID); + + if (!result.recordset[0]) { + throw new Error('Supplier not found'); + } + return supplierId; +} + +async function resolveArticleId(pool, tx, rootCategoryId, sku) { + const result = await request(pool, tx) + .input('rootCategoryId', rootCategoryId) + .input('sku', sku) + .query(RESOLVE_ARTICLE_ID_BY_SKU); + + const articleId = result.recordset[0]?.id; + if (!articleId) { + throw new Error('Article not found'); + } + return articleId; +} + +export async function insertLines( + pool, + tx, + { manifestId, rootCategoryId, lines, ensureTax }, +) { + for (const line of lines) { + const articleId = await resolveArticleId( + pool, + tx, + rootCategoryId, + line.sku, + ); + const taxId = await ensureTax(line.tax, tx); + + await request(pool, tx) + .input('manifestId', manifestId) + .input('articleId', articleId) + .input('amount', line.amount) + .input('net', line.net) + .input('transactedAmount', line.transactedAmount) + .input('comment', line.comment) + .input('rebate', line.rebate) + .input('taxId', taxId) + .query(CREATE_LINE); + } +}