diff --git a/docs/api/README.md b/docs/api/README.md index cf886da..561471f 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -65,11 +65,28 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/company/create.md`](routes/company/create.md) | `POST /company` | | [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` | | [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` | +| [`routes/customergroup/list.md`](routes/customergroup/list.md) | `GET /customergroup` | +| [`routes/customergroup/create.md`](routes/customergroup/create.md) | `POST /customergroup` | +| [`routes/customergroup/delete.md`](routes/customergroup/delete.md) | `DELETE /customergroup/{id}` | +| [`routes/customergroup/update.md`](routes/customergroup/update.md) | `PATCH /customergroup/{id}` | | [`routes/delivery/list.md`](routes/delivery/list.md) | `GET /delivery` | | [`routes/delivery/create.md`](routes/delivery/create.md) | `POST /delivery` | | [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` | | [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` | | [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` | +| [`routes/entity/list.md`](routes/entity/list.md) | `GET /entity` | +| [`routes/entity/create.md`](routes/entity/create.md) | `POST /entity` | +| [`routes/entity/address/list.md`](routes/entity/address/list.md) | `GET /entity/{entityId}/address` | +| [`routes/entity/address/create.md`](routes/entity/address/create.md) | `POST /entity/{entityId}/address` | +| [`routes/entity/address/delete.md`](routes/entity/address/delete.md) | `DELETE /entity/{entityId}/address/{id}` | +| [`routes/entity/address/update.md`](routes/entity/address/update.md) | `PATCH /entity/{entityId}/address/{id}` | +| [`routes/entity/contact/list.md`](routes/entity/contact/list.md) | `GET /entity/{entityId}/contact` | +| [`routes/entity/contact/create.md`](routes/entity/contact/create.md) | `POST /entity/{entityId}/contact` | +| [`routes/entity/contact/delete.md`](routes/entity/contact/delete.md) | `DELETE /entity/{entityId}/contact/{id}` | +| [`routes/entity/contact/update.md`](routes/entity/contact/update.md) | `PATCH /entity/{entityId}/contact/{id}` | +| [`routes/entity/delete.md`](routes/entity/delete.md) | `DELETE /entity/{id}` | +| [`routes/entity/get.md`](routes/entity/get.md) | `GET /entity/{id}` | +| [`routes/entity/update.md`](routes/entity/update.md) | `PATCH /entity/{id}` | | [`routes/events.md`](routes/events.md) | `GET /events` | | [`routes/health.md`](routes/health.md) | `GET /health` | | [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index b2a9417..9d123a4 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -52,6 +52,14 @@ "name": "Delivery", "description": "Company-scoped goods receipts (Lieferungen)" }, + { + "name": "Customergroup", + "description": "Company-scoped customer groups" + }, + { + "name": "Entity", + "description": "Company-scoped entities, addresses, and contacts" + }, { "name": "Image", "description": "Content-addressed AVIF by fullHash" @@ -1551,6 +1559,1255 @@ } } }, + "/entity/{entityId}/contact": { + "get": { + "tags": [ + "Entity" + ], + "summary": "List entity contacts", + "description": "Lists contacts for an entity of the selected company (`contactType` e.g. `email`, `phone`).", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/contact/list.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Contact list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityContact" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid entityId", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Entity" + ], + "summary": "Create entity contact", + "description": "Creates a contact for the entity. `contactType` and `contact` are required.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/contact/create.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "contactType", + "contact" + ], + "properties": { + "contactType": { + "type": "string", + "description": "e.g. email, phone" + }, + "contact": { + "type": "string" + } + }, + "example": { + "contactType": "email", + "contact": "anna@example.com" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created contact", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contact": { + "$ref": "#/components/schemas/EntityContact" + } + } + } + } + } + }, + "400": { + "description": "Contact required, invalid entityId, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entity/{entityId}/contact/{id}": { + "patch": { + "tags": [ + "Entity" + ], + "summary": "Update entity contact", + "description": "Patches `contactType` and/or `contact`.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/contact/update.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactType": { + "type": "string" + }, + "contact": { + "type": "string" + } + }, + "example": { + "contact": "anna.mueller@example.com" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated contact", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contact": { + "$ref": "#/components/schemas/EntityContact" + } + } + } + } + } + }, + "400": { + "description": "Contact required, invalid ids, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Contact, entity, or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Entity" + ], + "summary": "Delete entity contact", + "description": "Deletes one contact of the entity.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/contact/delete.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid ids", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Contact or entity not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entity/{entityId}/address": { + "get": { + "tags": [ + "Entity" + ], + "summary": "List entity addresses", + "description": "Lists addresses for an entity of the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/address/list.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Address list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityAddress" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid entityId", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Entity" + ], + "summary": "Create entity address", + "description": "Creates an address for the entity. `addressType` is required.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/address/create.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "addressType" + ], + "properties": { + "addressType": { + "type": "string" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "street": { + "type": "string", + "nullable": true + }, + "streetNumber": { + "type": "string", + "nullable": true + }, + "zip": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "county": { + "type": "string", + "nullable": true + } + }, + "example": { + "addressType": "billing", + "street": "Hauptstraße", + "streetNumber": "12", + "zip": "10115", + "city": "Berlin" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created address", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/EntityAddress" + } + } + } + } + } + }, + "400": { + "description": "Address type required, invalid entityId, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entity/{entityId}/address/{id}": { + "patch": { + "tags": [ + "Entity" + ], + "summary": "Update entity address", + "description": "Patches address fields that are present.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/address/update.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addressType": { + "type": "string" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "street": { + "type": "string", + "nullable": true + }, + "streetNumber": { + "type": "string", + "nullable": true + }, + "zip": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "county": { + "type": "string", + "nullable": true + } + }, + "example": { + "city": "München", + "zip": "80331" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated address", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/EntityAddress" + } + } + } + } + } + }, + "400": { + "description": "Address type required, invalid ids, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Address, entity, or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Entity" + ], + "summary": "Delete entity address", + "description": "Deletes one address of the entity.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/address/delete.md", + "parameters": [ + { + "in": "path", + "name": "entityId", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid ids", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Address or entity not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entity": { + "get": { + "tags": [ + "Entity" + ], + "summary": "List entities", + "description": "Lists entities for the selected company. Optional `customergroupId`\nquery filters to one group (must belong to the company).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/list.md", + "parameters": [ + { + "in": "query", + "name": "customergroupId", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Entity list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entity" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid customergroupId", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company or customer group not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Entity" + ], + "summary": "Create entity", + "description": "Creates an entity in a company customer group and writes `entityhistory` (`Erstellt`).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/create.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "customergroupId", + "name" + ], + "properties": { + "customergroupId": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "firstName": { + "type": "string", + "nullable": true + } + }, + "example": { + "customergroupId": 1, + "name": "Müller", + "firstName": "Anna" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created entity", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/components/schemas/Entity" + } + } + } + } + } + }, + "400": { + "description": "Name required or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company or customer group not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entity/{id}": { + "get": { + "tags": [ + "Entity" + ], + "summary": "Get entity", + "description": "Returns one entity for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/get.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Entity", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/components/schemas/Entity" + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Entity" + ], + "summary": "Update entity", + "description": "Patches `name` and/or `firstName`. Writes `entityhistory` when values change.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/update.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "firstName": { + "type": "string", + "nullable": true + } + }, + "example": { + "name": "Müller", + "firstName": "Anna" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated entity", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/components/schemas/Entity" + } + } + } + } + } + }, + "400": { + "description": "Name required, invalid id, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Entity" + ], + "summary": "Delete entity", + "description": "Deletes the entity (`entityhistory` cascades; related addresses/contacts cascade).", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "entity/delete.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Entity not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/delivery": { "get": { "tags": [ @@ -2006,6 +3263,353 @@ } } }, + "/customergroup": { + "get": { + "tags": [ + "Customergroup" + ], + "summary": "List customer groups", + "description": "Lists customer groups for the authenticated user's selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "customergroup/list.md", + "responses": { + "200": { + "description": "Customer group list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "customergroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Customergroup" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Customergroup" + ], + "summary": "Create customer group", + "description": "Creates a customer group for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "customergroup/create.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "example": { + "name": "Kunden" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created customer group", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "customergroup": { + "$ref": "#/components/schemas/Customergroup" + } + } + } + } + } + }, + "400": { + "description": "Name required or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/customergroup/{id}": { + "patch": { + "tags": [ + "Customergroup" + ], + "summary": "Update customer group", + "description": "Renames a customer group for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "customergroup/update.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "example": { + "name": "Stammkunden" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated customer group", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "customergroup": { + "$ref": "#/components/schemas/Customergroup" + } + } + } + } + } + }, + "400": { + "description": "Name required, invalid id, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Customer group or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Customergroup" + ], + "summary": "Delete customer group", + "description": "Deletes the customer group (cascades entities). Fails with\n`Customer group in use` for the POS group or other NO ACTION references.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "customergroup/delete.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Customer group not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Customer group in use", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/company": { "get": { "tags": [ @@ -5393,6 +6997,126 @@ } } }, + "Customergroup": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "Entity": { + "type": "object", + "required": [ + "id", + "customergroupId", + "firstName", + "name", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "integer" + }, + "customergroupId": { + "type": "integer" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "EntityAddress": { + "type": "object", + "required": [ + "id", + "entityId", + "addressType" + ], + "properties": { + "id": { + "type": "integer" + }, + "entityId": { + "type": "integer" + }, + "firstName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "street": { + "type": "string", + "nullable": true + }, + "streetNumber": { + "type": "string", + "nullable": true + }, + "zip": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "county": { + "type": "string", + "nullable": true + }, + "addressType": { + "type": "string" + } + } + }, + "EntityContact": { + "type": "object", + "required": [ + "id", + "entityId", + "contactType", + "contact" + ], + "properties": { + "id": { + "type": "integer" + }, + "entityId": { + "type": "integer" + }, + "contactType": { + "type": "string", + "description": "e.g. email, phone" + }, + "contact": { + "type": "string" + } + } + }, "DeliverySummary": { "type": "object", "required": [ diff --git a/docs/api/routes/customergroup/create.md b/docs/api/routes/customergroup/create.md new file mode 100644 index 0000000..117f01d --- /dev/null +++ b/docs/api/routes/customergroup/create.md @@ -0,0 +1,41 @@ +# `POST /customergroup` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create customer group + +Creates a customer group for the selected company. + +## Body (JSON) + +```json +{ + "name": "Kunden" +} +``` + +## Response `201` + +Created customer group + +```json +{ + "customergroup": { + "id": 1, + "name": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/customergroup/delete.md b/docs/api/routes/customergroup/delete.md new file mode 100644 index 0000000..4350392 --- /dev/null +++ b/docs/api/routes/customergroup/delete.md @@ -0,0 +1,35 @@ +# `DELETE /customergroup/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete customer group + +Deletes the customer group (cascades entities). Fails with +`Customer group in use` for the POS group or other NO ACTION references. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Customer group not found — `{"error":"…"}` | +| 409 | Customer group in use — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/customergroup/list.md b/docs/api/routes/customergroup/list.md new file mode 100644 index 0000000..e37d59e --- /dev/null +++ b/docs/api/routes/customergroup/list.md @@ -0,0 +1,35 @@ +# `GET /customergroup` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List customer groups + +Lists customer groups for the authenticated user's selected company. + +## Response `200` + +Customer group list + +```json +{ + "customergroups": [ + { + "id": 1, + "name": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/customergroup/update.md b/docs/api/routes/customergroup/update.md new file mode 100644 index 0000000..1e2412f --- /dev/null +++ b/docs/api/routes/customergroup/update.md @@ -0,0 +1,47 @@ +# `PATCH /customergroup/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update customer group + +Renames a customer group for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "name": "Stammkunden" +} +``` + +## Response `200` + +Updated customer group + +```json +{ + "customergroup": { + "id": 1, + "name": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required, invalid id, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Customer group or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/address/create.md b/docs/api/routes/entity/address/create.md new file mode 100644 index 0000000..bf900ef --- /dev/null +++ b/docs/api/routes/entity/address/create.md @@ -0,0 +1,59 @@ +# `POST /entity/{entityId}/address` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create entity address + +Creates an address for the entity. `addressType` is required. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | + +## Body (JSON) + +```json +{ + "addressType": "billing", + "street": "Hauptstraße", + "streetNumber": "12", + "zip": "10115", + "city": "Berlin" +} +``` + +## Response `201` + +Created address + +```json +{ + "address": { + "id": 1, + "entityId": 1, + "firstName": "…", + "name": "…", + "street": "…", + "streetNumber": "…", + "zip": "…", + "city": "…", + "county": "…", + "addressType": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Address type required, invalid entityId, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/address/delete.md b/docs/api/routes/entity/address/delete.md new file mode 100644 index 0000000..6d81434 --- /dev/null +++ b/docs/api/routes/entity/address/delete.md @@ -0,0 +1,33 @@ +# `DELETE /entity/{entityId}/address/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete entity address + +Deletes one address of the entity. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid ids — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Address or entity not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/address/list.md b/docs/api/routes/entity/address/list.md new file mode 100644 index 0000000..894c5db --- /dev/null +++ b/docs/api/routes/entity/address/list.md @@ -0,0 +1,49 @@ +# `GET /entity/{entityId}/address` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List entity addresses + +Lists addresses for an entity of the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | + +## Response `200` + +Address list + +```json +{ + "addresses": [ + { + "id": 1, + "entityId": 1, + "firstName": "…", + "name": "…", + "street": "…", + "streetNumber": "…", + "zip": "…", + "city": "…", + "county": "…", + "addressType": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid entityId — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/address/update.md b/docs/api/routes/entity/address/update.md new file mode 100644 index 0000000..b36834d --- /dev/null +++ b/docs/api/routes/entity/address/update.md @@ -0,0 +1,57 @@ +# `PATCH /entity/{entityId}/address/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update entity address + +Patches address fields that are present. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "city": "München", + "zip": "80331" +} +``` + +## Response `200` + +Updated address + +```json +{ + "address": { + "id": 1, + "entityId": 1, + "firstName": "…", + "name": "…", + "street": "…", + "streetNumber": "…", + "zip": "…", + "city": "…", + "county": "…", + "addressType": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Address type required, invalid ids, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Address, entity, or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/contact/create.md b/docs/api/routes/entity/contact/create.md new file mode 100644 index 0000000..2fb3c51 --- /dev/null +++ b/docs/api/routes/entity/contact/create.md @@ -0,0 +1,50 @@ +# `POST /entity/{entityId}/contact` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create entity contact + +Creates a contact for the entity. `contactType` and `contact` are required. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | + +## Body (JSON) + +```json +{ + "contactType": "email", + "contact": "anna@example.com" +} +``` + +## Response `201` + +Created contact + +```json +{ + "contact": { + "id": 1, + "entityId": 1, + "contactType": "…", + "contact": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Contact required, invalid entityId, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/contact/delete.md b/docs/api/routes/entity/contact/delete.md new file mode 100644 index 0000000..b373aaf --- /dev/null +++ b/docs/api/routes/entity/contact/delete.md @@ -0,0 +1,33 @@ +# `DELETE /entity/{entityId}/contact/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete entity contact + +Deletes one contact of the entity. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid ids — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Contact or entity not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/contact/list.md b/docs/api/routes/entity/contact/list.md new file mode 100644 index 0000000..b32a67e --- /dev/null +++ b/docs/api/routes/entity/contact/list.md @@ -0,0 +1,43 @@ +# `GET /entity/{entityId}/contact` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List entity contacts + +Lists contacts for an entity of the selected company (`contactType` e.g. `email`, `phone`). + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | + +## Response `200` + +Contact list + +```json +{ + "contacts": [ + { + "id": 1, + "entityId": 1, + "contactType": "…", + "contact": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid entityId — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/contact/update.md b/docs/api/routes/entity/contact/update.md new file mode 100644 index 0000000..c60f259 --- /dev/null +++ b/docs/api/routes/entity/contact/update.md @@ -0,0 +1,50 @@ +# `PATCH /entity/{entityId}/contact/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update entity contact + +Patches `contactType` and/or `contact`. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `entityId` | yes | integer | | +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "contact": "anna.mueller@example.com" +} +``` + +## Response `200` + +Updated contact + +```json +{ + "contact": { + "id": 1, + "entityId": 1, + "contactType": "…", + "contact": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Contact required, invalid ids, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Contact, entity, or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/create.md b/docs/api/routes/entity/create.md new file mode 100644 index 0000000..ef7aecf --- /dev/null +++ b/docs/api/routes/entity/create.md @@ -0,0 +1,48 @@ +# `POST /entity` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create entity + +Creates an entity in a company customer group and writes `entityhistory` (`Erstellt`). + + +## Body (JSON) + +```json +{ + "customergroupId": 1, + "name": "Müller", + "firstName": "Anna" +} +``` + +## Response `201` + +Created entity + +```json +{ + "entity": { + "id": 1, + "customergroupId": 1, + "firstName": "…", + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company or customer group not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/delete.md b/docs/api/routes/entity/delete.md new file mode 100644 index 0000000..9d62f29 --- /dev/null +++ b/docs/api/routes/entity/delete.md @@ -0,0 +1,32 @@ +# `DELETE /entity/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete entity + +Deletes the entity (`entityhistory` cascades; related addresses/contacts cascade). + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/get.md b/docs/api/routes/entity/get.md new file mode 100644 index 0000000..9a2b490 --- /dev/null +++ b/docs/api/routes/entity/get.md @@ -0,0 +1,43 @@ +# `GET /entity/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Get entity + +Returns one entity for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `200` + +Entity + +```json +{ + "entity": { + "id": 1, + "customergroupId": 1, + "firstName": "…", + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/list.md b/docs/api/routes/entity/list.md new file mode 100644 index 0000000..154e367 --- /dev/null +++ b/docs/api/routes/entity/list.md @@ -0,0 +1,47 @@ +# `GET /entity` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List entities + +Lists entities for the selected company. Optional `customergroupId` +query filters to one group (must belong to the company). + + +## Query parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `customergroupId` | no | integer | | + +## Response `200` + +Entity list + +```json +{ + "entities": [ + { + "id": 1, + "customergroupId": 1, + "firstName": "…", + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid customergroupId — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company or customer group not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/entity/update.md b/docs/api/routes/entity/update.md new file mode 100644 index 0000000..682ac56 --- /dev/null +++ b/docs/api/routes/entity/update.md @@ -0,0 +1,53 @@ +# `PATCH /entity/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update entity + +Patches `name` and/or `firstName`. Writes `entityhistory` when values change. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "name": "Müller", + "firstName": "Anna" +} +``` + +## Response `200` + +Updated entity + +```json +{ + "entity": { + "id": 1, + "customergroupId": 1, + "firstName": "…", + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required, invalid id, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Entity or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index d8ecfc2..89f4018 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -38,6 +38,8 @@ const options = { { name: 'Brand', description: 'Company-scoped brands' }, { name: 'Supplier', description: 'Company-scoped suppliers (Lieferanten)' }, { name: 'Delivery', description: 'Company-scoped goods receipts (Lieferungen)' }, + { name: 'Customergroup', description: 'Company-scoped customer groups' }, + { name: 'Entity', description: 'Company-scoped entities, addresses, and contacts' }, { name: 'Image', description: 'Content-addressed AVIF by fullHash' }, { name: 'Posterminal', description: 'Company-scoped POS terminals' }, ], diff --git a/src/api/openapi.js b/src/api/openapi.js index 975da3b..2be56a5 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -124,6 +124,77 @@ * updatedAt: * type: string * format: date-time + * Customergroup: + * type: object + * required: [id, name] + * properties: + * id: + * type: integer + * name: + * type: string + * Entity: + * type: object + * required: [id, customergroupId, firstName, name, createdAt, updatedAt] + * properties: + * id: + * type: integer + * customergroupId: + * type: integer + * firstName: + * type: string + * nullable: true + * name: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * EntityAddress: + * type: object + * required: [id, entityId, addressType] + * properties: + * id: + * type: integer + * entityId: + * type: integer + * firstName: + * type: string + * nullable: true + * name: + * type: string + * nullable: true + * street: + * type: string + * nullable: true + * streetNumber: + * type: string + * nullable: true + * zip: + * type: string + * nullable: true + * city: + * type: string + * nullable: true + * county: + * type: string + * nullable: true + * addressType: + * type: string + * EntityContact: + * type: object + * required: [id, entityId, contactType, contact] + * properties: + * id: + * type: integer + * entityId: + * type: integer + * contactType: + * type: string + * description: e.g. email, phone + * contact: + * type: string * DeliverySummary: * type: object * required: [id, number, type, supplierId, date] diff --git a/src/api/routes/customergroup.js b/src/api/routes/customergroup.js new file mode 100644 index 0000000..06c3b11 --- /dev/null +++ b/src/api/routes/customergroup.js @@ -0,0 +1,314 @@ +import * as db from '../../db/customergroup.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function customergroupErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Customer group not found') return 404; + if (error.message === 'Name required') return 400; + if (error.message === 'Customer group in use') return 409; + return 500; +} + +/** + * Registers customergroup routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /customergroup: + * get: + * tags: [Customergroup] + * summary: List customer groups + * description: Lists customer groups for the authenticated user's selected company. + * security: + * - cookieAuth: [] + * x-markdown: customergroup/list.md + * responses: + * 200: + * description: Customer group list + * content: + * application/json: + * schema: + * type: object + * properties: + * customergroups: + * type: array + * items: + * $ref: '#/components/schemas/Customergroup' + * 400: + * description: No company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Customergroup] + * summary: Create customer group + * description: Creates a customer group for the selected company. + * security: + * - cookieAuth: [] + * x-markdown: customergroup/create.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * example: + * name: Kunden + * responses: + * 201: + * description: Created customer group + * content: + * application/json: + * schema: + * type: object + * properties: + * customergroup: + * $ref: '#/components/schemas/Customergroup' + * 400: + * description: Name required or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/customergroup`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const customergroups = await db.list(req.user.id, selected.companyId); + res.status(200).json({ customergroups }); + } catch (error) { + const status = customergroupErrorStatus(error); + if (status === 500) console.error('List customergroups failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/customergroup`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { name } = req.body ?? {}; + + try { + const customergroup = await db.create(req.user.id, selected.companyId, { name }); + res.status(201).json({ customergroup }); + } catch (error) { + const status = customergroupErrorStatus(error); + if (status === 500) console.error('Create customergroup failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /customergroup/{id}: + * patch: + * tags: [Customergroup] + * summary: Update customer group + * description: Renames a customer group for the selected company. + * security: + * - cookieAuth: [] + * x-markdown: customergroup/update.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * example: + * name: Stammkunden + * responses: + * 200: + * description: Updated customer group + * content: + * application/json: + * schema: + * type: object + * properties: + * customergroup: + * $ref: '#/components/schemas/Customergroup' + * 400: + * description: Name required, invalid id, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Customer group or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Customergroup] + * summary: Delete customer group + * description: | + * Deletes the customer group (cascades entities). Fails with + * `Customer group in use` for the POS group or other NO ACTION references. + * security: + * - cookieAuth: [] + * x-markdown: customergroup/delete.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid id + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Customer group not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 409: + * description: Customer group in use + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/customergroup/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const { name } = req.body ?? {}; + + try { + const customergroup = await db.update(req.user.id, selected.companyId, id, { name }); + res.status(200).json({ customergroup }); + } catch (error) { + const status = customergroupErrorStatus(error); + if (status === 500) console.error('Update customergroup failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/customergroup/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + try { + await db.remove(req.user.id, selected.companyId, id); + res.sendStatus(204); + } catch (error) { + const status = customergroupErrorStatus(error); + if (status === 500) console.error('Delete customergroup failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/entity.js b/src/api/routes/entity.js new file mode 100644 index 0000000..fc45f22 --- /dev/null +++ b/src/api/routes/entity.js @@ -0,0 +1,414 @@ +import * as db from '../../db/entity.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function entityErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Entity not found') return 404; + if (error.message === 'Customer group not found') return 404; + if (error.message === 'Name required') return 400; + return 500; +} + +/** + * Registers entity routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /entity: + * get: + * tags: [Entity] + * summary: List entities + * description: | + * Lists entities for the selected company. Optional `customergroupId` + * query filters to one group (must belong to the company). + * security: + * - cookieAuth: [] + * x-markdown: entity/list.md + * parameters: + * - in: query + * name: customergroupId + * required: false + * schema: + * type: integer + * responses: + * 200: + * description: Entity list + * content: + * application/json: + * schema: + * type: object + * properties: + * entities: + * type: array + * items: + * $ref: '#/components/schemas/Entity' + * 400: + * description: No company selected or invalid customergroupId + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company or customer group not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Entity] + * summary: Create entity + * description: | + * Creates an entity in a company customer group and writes `entityhistory` (`Erstellt`). + * security: + * - cookieAuth: [] + * x-markdown: entity/create.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [customergroupId, name] + * properties: + * customergroupId: + * type: integer + * name: + * type: string + * firstName: + * type: string + * nullable: true + * example: + * customergroupId: 1 + * name: Müller + * firstName: Anna + * responses: + * 201: + * description: Created entity + * content: + * application/json: + * schema: + * type: object + * properties: + * entity: + * $ref: '#/components/schemas/Entity' + * 400: + * description: Name required or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company or customer group not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/entity`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const options = {}; + if (Object.prototype.hasOwnProperty.call(req.query, 'customergroupId')) { + const customergroupId = Number(req.query.customergroupId); + if (!Number.isInteger(customergroupId) || customergroupId <= 0) { + return res.status(400).json({ error: 'customergroupId is invalid' }); + } + options.customergroupId = customergroupId; + } + + try { + const entities = await db.list(req.user.id, selected.companyId, options); + res.status(200).json({ entities }); + } catch (error) { + const status = entityErrorStatus(error); + if (status === 500) console.error('List entities failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/entity`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { customergroupId, name, firstName } = req.body ?? {}; + + try { + const entity = await db.create(req.user.id, selected.companyId, { + customergroupId, + name, + firstName, + }); + res.status(201).json({ entity }); + } catch (error) { + const status = entityErrorStatus(error); + if (status === 500) console.error('Create entity failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /entity/{id}: + * get: + * tags: [Entity] + * summary: Get entity + * description: Returns one entity for the selected company. + * security: + * - cookieAuth: [] + * x-markdown: entity/get.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Entity + * content: + * application/json: + * schema: + * type: object + * properties: + * entity: + * $ref: '#/components/schemas/Entity' + * 400: + * description: No company selected or invalid id + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * patch: + * tags: [Entity] + * summary: Update entity + * description: | + * Patches `name` and/or `firstName`. Writes `entityhistory` when values change. + * security: + * - cookieAuth: [] + * x-markdown: entity/update.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * firstName: + * type: string + * nullable: true + * example: + * name: Müller + * firstName: Anna + * responses: + * 200: + * description: Updated entity + * content: + * application/json: + * schema: + * type: object + * properties: + * entity: + * $ref: '#/components/schemas/Entity' + * 400: + * description: Name required, invalid id, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Entity] + * summary: Delete entity + * description: Deletes the entity (`entityhistory` cascades; related addresses/contacts cascade). + * security: + * - cookieAuth: [] + * x-markdown: entity/delete.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid id + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/entity/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + try { + const entity = await db.getById(req.user.id, selected.companyId, id); + res.status(200).json({ entity }); + } catch (error) { + const status = entityErrorStatus(error); + if (status === 500) console.error('Get entity failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.patch(`${basePath}/entity/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const body = req.body ?? {}; + const patch = {}; + if (Object.prototype.hasOwnProperty.call(body, 'name')) { + patch.name = body.name; + } + if (Object.prototype.hasOwnProperty.call(body, 'firstName')) { + patch.firstName = body.firstName; + } + + try { + const entity = await db.update(req.user.id, selected.companyId, id, patch); + res.status(200).json({ entity }); + } catch (error) { + const status = entityErrorStatus(error); + if (status === 500) console.error('Update entity failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/entity/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + try { + await db.remove(req.user.id, selected.companyId, id); + res.sendStatus(204); + } catch (error) { + const status = entityErrorStatus(error); + if (status === 500) console.error('Delete entity failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/entityaddress.js b/src/api/routes/entityaddress.js new file mode 100644 index 0000000..45d84ca --- /dev/null +++ b/src/api/routes/entityaddress.js @@ -0,0 +1,415 @@ +import * as db from '../../db/entityaddress.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function addressErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Entity not found') return 404; + if (error.message === 'Address not found') return 404; + if (error.message === 'Address type required') return 400; + return 500; +} + +const ADDRESS_FIELDS = [ + 'addressType', + 'firstName', + 'name', + 'street', + 'streetNumber', + 'zip', + 'city', + 'county', +]; + +/** + * Registers entity address routes nested under `/entity/:entityId/address`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /entity/{entityId}/address: + * get: + * tags: [Entity] + * summary: List entity addresses + * description: Lists addresses for an entity of the selected company. + * security: + * - cookieAuth: [] + * x-markdown: entity/address/list.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Address list + * content: + * application/json: + * schema: + * type: object + * properties: + * addresses: + * type: array + * items: + * $ref: '#/components/schemas/EntityAddress' + * 400: + * description: No company selected or invalid entityId + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Entity] + * summary: Create entity address + * description: Creates an address for the entity. `addressType` is required. + * security: + * - cookieAuth: [] + * x-markdown: entity/address/create.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [addressType] + * properties: + * addressType: + * type: string + * firstName: + * type: string + * nullable: true + * name: + * type: string + * nullable: true + * street: + * type: string + * nullable: true + * streetNumber: + * type: string + * nullable: true + * zip: + * type: string + * nullable: true + * city: + * type: string + * nullable: true + * county: + * type: string + * nullable: true + * example: + * addressType: billing + * street: Hauptstraße + * streetNumber: '12' + * zip: '10115' + * city: Berlin + * responses: + * 201: + * description: Created address + * content: + * application/json: + * schema: + * type: object + * properties: + * address: + * $ref: '#/components/schemas/EntityAddress' + * 400: + * description: Address type required, invalid entityId, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/entity/:entityId/address`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + + try { + const addresses = await db.list(req.user.id, selected.companyId, entityId); + res.status(200).json({ addresses }); + } catch (error) { + const status = addressErrorStatus(error); + if (status === 500) console.error('List entity addresses failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/entity/:entityId/address`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + + const body = req.body ?? {}; + const data = {}; + for (const key of ADDRESS_FIELDS) { + if (Object.prototype.hasOwnProperty.call(body, key)) { + data[key] = body[key]; + } + } + + try { + const address = await db.create(req.user.id, selected.companyId, entityId, data); + res.status(201).json({ address }); + } catch (error) { + const status = addressErrorStatus(error); + if (status === 500) console.error('Create entity address failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /entity/{entityId}/address/{id}: + * patch: + * tags: [Entity] + * summary: Update entity address + * description: Patches address fields that are present. + * security: + * - cookieAuth: [] + * x-markdown: entity/address/update.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * addressType: + * type: string + * firstName: + * type: string + * nullable: true + * name: + * type: string + * nullable: true + * street: + * type: string + * nullable: true + * streetNumber: + * type: string + * nullable: true + * zip: + * type: string + * nullable: true + * city: + * type: string + * nullable: true + * county: + * type: string + * nullable: true + * example: + * city: München + * zip: '80331' + * responses: + * 200: + * description: Updated address + * content: + * application/json: + * schema: + * type: object + * properties: + * address: + * $ref: '#/components/schemas/EntityAddress' + * 400: + * description: Address type required, invalid ids, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Address, entity, or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Entity] + * summary: Delete entity address + * description: Deletes one address of the entity. + * security: + * - cookieAuth: [] + * x-markdown: entity/address/delete.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid ids + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Address or entity not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/entity/:entityId/address/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + const id = Number(req.params.id); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const body = req.body ?? {}; + const patch = {}; + for (const key of ADDRESS_FIELDS) { + if (Object.prototype.hasOwnProperty.call(body, key)) { + patch[key] = body[key]; + } + } + + try { + const address = await db.update(req.user.id, selected.companyId, entityId, id, patch); + res.status(200).json({ address }); + } catch (error) { + const status = addressErrorStatus(error); + if (status === 500) console.error('Update entity address failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/entity/:entityId/address/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + const id = Number(req.params.id); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + try { + await db.remove(req.user.id, selected.companyId, entityId, id); + res.sendStatus(204); + } catch (error) { + const status = addressErrorStatus(error); + if (status === 500) console.error('Delete entity address failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/entitycontact.js b/src/api/routes/entitycontact.js new file mode 100644 index 0000000..1035841 --- /dev/null +++ b/src/api/routes/entitycontact.js @@ -0,0 +1,361 @@ +import * as db from '../../db/entitycontact.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function contactErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Entity not found') return 404; + if (error.message === 'Contact not found') return 404; + if (error.message === 'Contact required') return 400; + return 500; +} + +/** + * Registers entity contact routes nested under `/entity/:entityId/contact`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /entity/{entityId}/contact: + * get: + * tags: [Entity] + * summary: List entity contacts + * description: Lists contacts for an entity of the selected company (`contactType` e.g. `email`, `phone`). + * security: + * - cookieAuth: [] + * x-markdown: entity/contact/list.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Contact list + * content: + * application/json: + * schema: + * type: object + * properties: + * contacts: + * type: array + * items: + * $ref: '#/components/schemas/EntityContact' + * 400: + * description: No company selected or invalid entityId + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Entity] + * summary: Create entity contact + * description: Creates a contact for the entity. `contactType` and `contact` are required. + * security: + * - cookieAuth: [] + * x-markdown: entity/contact/create.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [contactType, contact] + * properties: + * contactType: + * type: string + * description: e.g. email, phone + * contact: + * type: string + * example: + * contactType: email + * contact: anna@example.com + * responses: + * 201: + * description: Created contact + * content: + * application/json: + * schema: + * type: object + * properties: + * contact: + * $ref: '#/components/schemas/EntityContact' + * 400: + * description: Contact required, invalid entityId, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Entity or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/entity/:entityId/contact`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + + try { + const contacts = await db.list(req.user.id, selected.companyId, entityId); + res.status(200).json({ contacts }); + } catch (error) { + const status = contactErrorStatus(error); + if (status === 500) console.error('List entity contacts failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/entity/:entityId/contact`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + + const { contactType, contact } = req.body ?? {}; + + try { + const row = await db.create(req.user.id, selected.companyId, entityId, { + contactType, + contact, + }); + res.status(201).json({ contact: row }); + } catch (error) { + const status = contactErrorStatus(error); + if (status === 500) console.error('Create entity contact failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /entity/{entityId}/contact/{id}: + * patch: + * tags: [Entity] + * summary: Update entity contact + * description: Patches `contactType` and/or `contact`. + * security: + * - cookieAuth: [] + * x-markdown: entity/contact/update.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * contactType: + * type: string + * contact: + * type: string + * example: + * contact: anna.mueller@example.com + * responses: + * 200: + * description: Updated contact + * content: + * application/json: + * schema: + * type: object + * properties: + * contact: + * $ref: '#/components/schemas/EntityContact' + * 400: + * description: Contact required, invalid ids, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Contact, entity, or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Entity] + * summary: Delete entity contact + * description: Deletes one contact of the entity. + * security: + * - cookieAuth: [] + * x-markdown: entity/contact/delete.md + * parameters: + * - in: path + * name: entityId + * required: true + * schema: + * type: integer + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid ids + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Contact or entity not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/entity/:entityId/contact/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + const id = Number(req.params.id); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const body = req.body ?? {}; + const patch = {}; + if (Object.prototype.hasOwnProperty.call(body, 'contactType')) { + patch.contactType = body.contactType; + } + if (Object.prototype.hasOwnProperty.call(body, 'contact')) { + patch.contact = body.contact; + } + + try { + const row = await db.update(req.user.id, selected.companyId, entityId, id, patch); + res.status(200).json({ contact: row }); + } catch (error) { + const status = contactErrorStatus(error); + if (status === 500) console.error('Update entity contact failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/entity/:entityId/contact/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const entityId = Number(req.params.entityId); + const id = Number(req.params.id); + if (!Number.isInteger(entityId) || entityId <= 0) { + return res.status(400).json({ error: 'entityId is required' }); + } + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + try { + await db.remove(req.user.id, selected.companyId, entityId, id); + res.sendStatus(204); + } catch (error) { + const status = contactErrorStatus(error); + if (status === 500) console.error('Delete entity contact failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/customergroup.js b/src/db/customergroup.js new file mode 100644 index 0000000..db242e2 --- /dev/null +++ b/src/db/customergroup.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId) { + const store = await getStore(); + return store.customergroup.list(googleAuthId, companyId); +} + +export async function create(googleAuthId, companyId, data) { + const store = await getStore(); + return store.customergroup.create(googleAuthId, companyId, data); +} + +export async function update(googleAuthId, companyId, id, data) { + const store = await getStore(); + return store.customergroup.update(googleAuthId, companyId, id, data); +} + +export async function remove(googleAuthId, companyId, id) { + const store = await getStore(); + return store.customergroup.delete(googleAuthId, companyId, id); +} diff --git a/src/db/entity.js b/src/db/entity.js new file mode 100644 index 0000000..3782cb8 --- /dev/null +++ b/src/db/entity.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId, options) { + const store = await getStore(); + return store.entity.list(googleAuthId, companyId, options); +} + +export async function getById(googleAuthId, companyId, id) { + const store = await getStore(); + return store.entity.getById(googleAuthId, companyId, id); +} + +export async function create(googleAuthId, companyId, data) { + const store = await getStore(); + return store.entity.create(googleAuthId, companyId, data); +} + +export async function update(googleAuthId, companyId, id, data) { + const store = await getStore(); + return store.entity.update(googleAuthId, companyId, id, data); +} + +export async function remove(googleAuthId, companyId, id) { + const store = await getStore(); + return store.entity.delete(googleAuthId, companyId, id); +} diff --git a/src/db/entityaddress.js b/src/db/entityaddress.js new file mode 100644 index 0000000..5c3ecfc --- /dev/null +++ b/src/db/entityaddress.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId, entityId) { + const store = await getStore(); + return store.entityaddress.list(googleAuthId, companyId, entityId); +} + +export async function create(googleAuthId, companyId, entityId, data) { + const store = await getStore(); + return store.entityaddress.create(googleAuthId, companyId, entityId, data); +} + +export async function update(googleAuthId, companyId, entityId, id, data) { + const store = await getStore(); + return store.entityaddress.update(googleAuthId, companyId, entityId, id, data); +} + +export async function remove(googleAuthId, companyId, entityId, id) { + const store = await getStore(); + return store.entityaddress.delete(googleAuthId, companyId, entityId, id); +} diff --git a/src/db/entitycontact.js b/src/db/entitycontact.js new file mode 100644 index 0000000..39ad158 --- /dev/null +++ b/src/db/entitycontact.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId, entityId) { + const store = await getStore(); + return store.entitycontact.list(googleAuthId, companyId, entityId); +} + +export async function create(googleAuthId, companyId, entityId, data) { + const store = await getStore(); + return store.entitycontact.create(googleAuthId, companyId, entityId, data); +} + +export async function update(googleAuthId, companyId, entityId, id, data) { + const store = await getStore(); + return store.entitycontact.update(googleAuthId, companyId, entityId, id, data); +} + +export async function remove(googleAuthId, companyId, entityId, id) { + const store = await getStore(); + return store.entitycontact.delete(googleAuthId, companyId, entityId, id); +} diff --git a/src/db/store.js b/src/db/store.js index af7cb3a..167cb9c 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -21,6 +21,10 @@ const STORE_NAMESPACES = [ 'posterminal', 'supplier', 'delivery', + 'customergroup', + 'entity', + 'entityaddress', + 'entitycontact', ]; /**