u
This commit is contained in:
@@ -241,6 +241,20 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/quote/_id/file/_fileId.get.md`](routes/quote/_id/file/_fileId.get.md) | `GET /quote/{id}/file/{fileId}` |
|
| [`routes/quote/_id/file/_fileId.get.md`](routes/quote/_id/file/_fileId.get.md) | `GET /quote/{id}/file/{fileId}` |
|
||||||
| [`routes/quote/order.md`](routes/quote/order.md) | `POST /quote/{id}/order` |
|
| [`routes/quote/order.md`](routes/quote/order.md) | `POST /quote/{id}/order` |
|
||||||
| [`routes/quote/_id/pdf.md`](routes/quote/_id/pdf.md) | `POST /quote/{id}/pdf` |
|
| [`routes/quote/_id/pdf.md`](routes/quote/_id/pdf.md) | `POST /quote/{id}/pdf` |
|
||||||
|
| [`routes/retoure.get.md`](routes/retoure.get.md) | `GET /retoure` |
|
||||||
|
| [`routes/retoure.post.md`](routes/retoure.post.md) | `POST /retoure` |
|
||||||
|
| [`routes/retoure/rebuild-summaries.md`](routes/retoure/rebuild-summaries.md) | `POST /retoure/rebuild-summaries` |
|
||||||
|
| [`routes/retoure/_id.delete.md`](routes/retoure/_id.delete.md) | `DELETE /retoure/{id}` |
|
||||||
|
| [`routes/retoure/_id.get.md`](routes/retoure/_id.get.md) | `GET /retoure/{id}` |
|
||||||
|
| [`routes/retoure/_id.put.md`](routes/retoure/_id.put.md) | `PUT /retoure/{id}` |
|
||||||
|
| [`routes/retoure/_id/shipment.md`](routes/retoure/_id/shipment.md) | `POST /retoure/{id}/shipment` |
|
||||||
|
| [`routes/return.get.md`](routes/return.get.md) | `GET /return` |
|
||||||
|
| [`routes/return.post.md`](routes/return.post.md) | `POST /return` |
|
||||||
|
| [`routes/return/rebuild-summaries.md`](routes/return/rebuild-summaries.md) | `POST /return/rebuild-summaries` |
|
||||||
|
| [`routes/return/_id.delete.md`](routes/return/_id.delete.md) | `DELETE /return/{id}` |
|
||||||
|
| [`routes/return/_id.get.md`](routes/return/_id.get.md) | `GET /return/{id}` |
|
||||||
|
| [`routes/return/_id.put.md`](routes/return/_id.put.md) | `PUT /return/{id}` |
|
||||||
|
| [`routes/return/_id/book.md`](routes/return/_id/book.md) | `POST /return/{id}/book` |
|
||||||
| [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` |
|
| [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` |
|
||||||
| [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` |
|
| [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` |
|
||||||
| [`routes/shipment.md`](routes/shipment.md) | `GET /shipment` |
|
| [`routes/shipment.md`](routes/shipment.md) | `GET /shipment` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 270 API endpoints across all modules.
|
Overview of all 284 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -787,6 +787,54 @@ _none_
|
|||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 { attachment: T9, fileUrl: str }`
|
- Returns: `201 { attachment: T9, fileUrl: str }`
|
||||||
|
|
||||||
|
### Section: /retoure
|
||||||
|
|
||||||
|
#### List retoures — `GET /retoure`
|
||||||
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, entityId?: int, seekMonths?: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create retoure — `POST /retoure`
|
||||||
|
- Params: `body: { date: str, number?: str, entityId?: int, externalNumber?: str, lines?: any[] }`
|
||||||
|
- Returns: `201`
|
||||||
|
#### Rebuild retoure summaries — `POST /retoure/rebuild-summaries`
|
||||||
|
_none_
|
||||||
|
- Returns: `200`
|
||||||
|
#### Delete retoure — `DELETE /retoure/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
#### Get retoure by ID — `GET /retoure/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Update retoure — `PUT /retoure/{id}`
|
||||||
|
- Params: `id: int, body: {}`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create shipment for retoure — `POST /retoure/{id}/shipment`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `201`
|
||||||
|
|
||||||
|
### Section: /return
|
||||||
|
|
||||||
|
#### List returns — `GET /return`
|
||||||
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create return — `POST /return`
|
||||||
|
- Params: `body: {}`
|
||||||
|
- Returns: `201`
|
||||||
|
#### Rebuild return summaries — `POST /return/rebuild-summaries`
|
||||||
|
_none_
|
||||||
|
- Returns: `200`
|
||||||
|
#### Delete return — `DELETE /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
#### Get return by ID — `GET /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Update return — `PUT /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Book return to ledger — `POST /return/{id}/book`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
|
||||||
### Section: /serper
|
### Section: /serper
|
||||||
|
|
||||||
#### Crawl image from URL, crop white/transparency borders, and return AVIF — `POST /serper/image`
|
#### Crawl image from URL, crop white/transparency borders, and return AVIF — `POST /serper/image`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview
|
# Dash API Routes Overview
|
||||||
|
|
||||||
Overview of all 190 API endpoints across all modules.
|
Overview of all 204 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -554,6 +554,54 @@ _none_
|
|||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `201 { attachment: T7, fileUrl: str }`
|
- Returns: `201 { attachment: T7, fileUrl: str }`
|
||||||
|
|
||||||
|
### Section: /retoure
|
||||||
|
|
||||||
|
#### List retoures — `GET /retoure`
|
||||||
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, entityId?: int, seekMonths?: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create retoure — `POST /retoure`
|
||||||
|
- Params: `body: { date: str, number?: str, entityId?: int, externalNumber?: str, lines?: any[] }`
|
||||||
|
- Returns: `201`
|
||||||
|
#### Rebuild retoure summaries — `POST /retoure/rebuild-summaries`
|
||||||
|
_none_
|
||||||
|
- Returns: `200`
|
||||||
|
#### Delete retoure — `DELETE /retoure/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
#### Get retoure by ID — `GET /retoure/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Update retoure — `PUT /retoure/{id}`
|
||||||
|
- Params: `id: int, body: {}`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create shipment for retoure — `POST /retoure/{id}/shipment`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `201`
|
||||||
|
|
||||||
|
### Section: /return
|
||||||
|
|
||||||
|
#### List returns — `GET /return`
|
||||||
|
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Create return — `POST /return`
|
||||||
|
- Params: `body: {}`
|
||||||
|
- Returns: `201`
|
||||||
|
#### Rebuild return summaries — `POST /return/rebuild-summaries`
|
||||||
|
_none_
|
||||||
|
- Returns: `200`
|
||||||
|
#### Delete return — `DELETE /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `204 void`
|
||||||
|
#### Get return by ID — `GET /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Update return — `PUT /return/{id}`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Book return to ledger — `POST /return/{id}/book`
|
||||||
|
- Params: `id: int`
|
||||||
|
- Returns: `200`
|
||||||
|
|
||||||
### Section: /shipment
|
### Section: /shipment
|
||||||
|
|
||||||
#### List shipments — `GET /shipment`
|
#### List shipments — `GET /shipment`
|
||||||
|
|||||||
36
docs/api/routes/retoure.get.md
Normal file
36
docs/api/routes/retoure.get.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# `GET /retoure`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
List retoures
|
||||||
|
|
||||||
|
Lists customer return manifests (`manifest` rows with `type = 'retoure'`)
|
||||||
|
for the authenticated user's selected company.
|
||||||
|
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `offset` | no | integer | |
|
||||||
|
| `limit` | no | integer | |
|
||||||
|
| `search` | no | string | |
|
||||||
|
| `onlyUnpaid` | no | boolean | |
|
||||||
|
| `onlyUndelivered` | no | boolean | |
|
||||||
|
| `entityId` | no | integer | |
|
||||||
|
| `seekMonths` | no | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
List of retoures
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Validation error |
|
||||||
|
| 403 | Not authorized |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
33
docs/api/routes/retoure.post.md
Normal file
33
docs/api/routes/retoure.post.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# `POST /retoure`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Create retoure
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
| Field | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `number` | no | string | |
|
||||||
|
| `date` | yes | string | |
|
||||||
|
| `entityId` | no | integer | |
|
||||||
|
| `externalNumber` | no | string | |
|
||||||
|
| `lines` | no | any[] | |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"number": "…",
|
||||||
|
"date": "…",
|
||||||
|
"entityId": 1,
|
||||||
|
"externalNumber": "…",
|
||||||
|
"lines": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `201`
|
||||||
|
|
||||||
|
Retoure created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
21
docs/api/routes/retoure/_id.delete.md
Normal file
21
docs/api/routes/retoure/_id.delete.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# `DELETE /retoure/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Delete retoure
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `204`
|
||||||
|
|
||||||
|
Retoure deleted
|
||||||
|
|
||||||
|
_No content_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
25
docs/api/routes/retoure/_id.get.md
Normal file
25
docs/api/routes/retoure/_id.get.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# `GET /retoure/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Get retoure by ID
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Retoure details
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 404 | Retoure not found |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
25
docs/api/routes/retoure/_id.put.md
Normal file
25
docs/api/routes/retoure/_id.put.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# `PUT /retoure/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Update retoure
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Retoure updated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
19
docs/api/routes/retoure/_id/shipment.md
Normal file
19
docs/api/routes/retoure/_id/shipment.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# `POST /retoure/{id}/shipment`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Create shipment for retoure
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `201`
|
||||||
|
|
||||||
|
Shipment created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
13
docs/api/routes/retoure/rebuild-summaries.md
Normal file
13
docs/api/routes/retoure/rebuild-summaries.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# `POST /retoure/rebuild-summaries`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Rebuild retoure summaries
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Summaries rebuilt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
23
docs/api/routes/return.get.md
Normal file
23
docs/api/routes/return.get.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# `GET /return`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
List returns
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `offset` | no | integer | |
|
||||||
|
| `limit` | no | integer | |
|
||||||
|
| `search` | no | string | |
|
||||||
|
| `onlyUnbooked` | no | boolean | |
|
||||||
|
| `supplierId` | no | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
List of returns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
19
docs/api/routes/return.post.md
Normal file
19
docs/api/routes/return.post.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# `POST /return`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Create return
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `201`
|
||||||
|
|
||||||
|
Return created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
21
docs/api/routes/return/_id.delete.md
Normal file
21
docs/api/routes/return/_id.delete.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# `DELETE /return/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Delete return
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `204`
|
||||||
|
|
||||||
|
Return deleted
|
||||||
|
|
||||||
|
_No content_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
19
docs/api/routes/return/_id.get.md
Normal file
19
docs/api/routes/return/_id.get.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# `GET /return/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Get return by ID
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Return details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
19
docs/api/routes/return/_id.put.md
Normal file
19
docs/api/routes/return/_id.put.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# `PUT /return/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Update return
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Return updated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
19
docs/api/routes/return/_id/book.md
Normal file
19
docs/api/routes/return/_id/book.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# `POST /return/{id}/book`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Book return to ledger
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Return booked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
13
docs/api/routes/return/rebuild-summaries.md
Normal file
13
docs/api/routes/return/rebuild-summaries.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# `POST /return/rebuild-summaries`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Rebuild return summaries
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Summaries rebuilt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
359
src/api/routes/retoure.js
Normal file
359
src/api/routes/retoure.js
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import * as db from '../../db/retoure.js';
|
||||||
|
import { emit } from '../sse.js';
|
||||||
|
|
||||||
|
// #region Helpers
|
||||||
|
|
||||||
|
function requireSelectedCompany(req) {
|
||||||
|
const companyId = req.user.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
|
return { error: 'No company selected' };
|
||||||
|
}
|
||||||
|
return { companyId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function retoureErrorStatus(error) {
|
||||||
|
if (error.message === 'No company selected') return 400;
|
||||||
|
if (error.message === 'Not authorized') return 403;
|
||||||
|
if (error.message === 'Company not found') return 404;
|
||||||
|
if (error.message === 'Retoure not found') return 404;
|
||||||
|
if (error.message === 'Manifest not found') return 404;
|
||||||
|
if (error.message === 'Entity not found') return 404;
|
||||||
|
if (error.message === 'Article not found') return 404;
|
||||||
|
if (error.message === 'No entity selected') return 400;
|
||||||
|
if (error.message === 'Retoure number already exists') return 409;
|
||||||
|
if (error.message === 'Invalid entityId') return 400;
|
||||||
|
if (error.message === 'Invalid retoure') return 400;
|
||||||
|
if (error.message === 'Invalid lines') return 400;
|
||||||
|
if (error.message === 'Invalid offset') return 400;
|
||||||
|
if (error.message === 'Invalid limit') return 400;
|
||||||
|
if (error.message === 'Invalid seekMonths') return 400;
|
||||||
|
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers retoure (customer return) routes for the authenticated user's selected company.
|
||||||
|
* @param {import('express').Application} app
|
||||||
|
* @param {string} [basePath='']
|
||||||
|
*/
|
||||||
|
export function register(app, basePath = '') {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure:
|
||||||
|
* get:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: List retoures
|
||||||
|
* description: |
|
||||||
|
* Lists customer return manifests (`manifest` rows with `type = 'retoure'`)
|
||||||
|
* for the authenticated user's selected company.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: offset
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 50
|
||||||
|
* - in: query
|
||||||
|
* name: search
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: onlyUnpaid
|
||||||
|
* schema:
|
||||||
|
* type: boolean
|
||||||
|
* - in: query
|
||||||
|
* name: onlyUndelivered
|
||||||
|
* schema:
|
||||||
|
* type: boolean
|
||||||
|
* - in: query
|
||||||
|
* name: entityId
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* - in: query
|
||||||
|
* name: seekMonths
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of retoures
|
||||||
|
* 400:
|
||||||
|
* description: Validation error
|
||||||
|
* 403:
|
||||||
|
* description: Not authorized
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/retoure`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {};
|
||||||
|
if (req.query.offset !== undefined) opts.offset = req.query.offset;
|
||||||
|
if (req.query.limit !== undefined) opts.limit = req.query.limit;
|
||||||
|
if (typeof req.query.search === 'string') opts.search = req.query.search;
|
||||||
|
if (req.query.onlyUnpaid !== undefined) opts.onlyUnpaid = req.query.onlyUnpaid === 'true' || req.query.onlyUnpaid === '1';
|
||||||
|
if (req.query.onlyUndelivered !== undefined) opts.onlyUndelivered = req.query.onlyUndelivered === 'true' || req.query.onlyUndelivered === '1';
|
||||||
|
if (req.query.entityId !== undefined) opts.entityId = req.query.entityId;
|
||||||
|
if (req.query.seekMonths !== undefined) opts.seekMonths = Number(req.query.seekMonths);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.list(req.user.id, opts);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('List retoures failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure/{id}:
|
||||||
|
* get:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Get retoure by ID
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Retoure details
|
||||||
|
* 404:
|
||||||
|
* description: Retoure not found
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/retoure/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const retoure = await db.getById(req.user.id, id);
|
||||||
|
res.status(200).json({ retoure });
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Get retoure failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure:
|
||||||
|
* post:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Create retoure
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [date]
|
||||||
|
* properties:
|
||||||
|
* number:
|
||||||
|
* type: string
|
||||||
|
* date:
|
||||||
|
* type: string
|
||||||
|
* entityId:
|
||||||
|
* type: integer
|
||||||
|
* externalNumber:
|
||||||
|
* type: string
|
||||||
|
* lines:
|
||||||
|
* type: array
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Retoure created
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/retoure`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const retoure = await db.create(req.user.id, req.body);
|
||||||
|
emit('retoure', { companyId: companyCheck.companyId });
|
||||||
|
res.status(201).json({ retoure });
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Create retoure failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure/{id}:
|
||||||
|
* put:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Update retoure
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Retoure updated
|
||||||
|
*/
|
||||||
|
app.put(`${basePath}/retoure/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const retoure = await db.update(req.user.id, id, req.body);
|
||||||
|
emit('retoure', { companyId: companyCheck.companyId });
|
||||||
|
res.status(200).json({ retoure });
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Update retoure failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure/{id}:
|
||||||
|
* delete:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Delete retoure
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 204:
|
||||||
|
* description: Retoure deleted
|
||||||
|
*/
|
||||||
|
app.delete(`${basePath}/retoure/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.remove(req.user.id, id);
|
||||||
|
emit('retoure', { companyId: companyCheck.companyId });
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Delete retoure failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure/{id}/shipment:
|
||||||
|
* post:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Create shipment for retoure
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Shipment created
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/retoure/:id/shipment`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shipment = await db.createShipment(req.user.id, id, req.body);
|
||||||
|
emit('retoure', { companyId: companyCheck.companyId });
|
||||||
|
emit('shipment', { companyId: companyCheck.companyId });
|
||||||
|
res.status(201).json({ shipment });
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Create shipment for retoure failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /retoure/rebuild-summaries:
|
||||||
|
* post:
|
||||||
|
* tags: [Retoure]
|
||||||
|
* summary: Rebuild retoure summaries
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Summaries rebuilt
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/retoure/rebuild-summaries`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.rebuildSummaries(req.user.id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
const status = retoureErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Rebuild retoure summaries failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
319
src/api/routes/return.js
Normal file
319
src/api/routes/return.js
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import * as db from '../../db/return.js';
|
||||||
|
import { emit } from '../sse.js';
|
||||||
|
|
||||||
|
// #region Helpers
|
||||||
|
|
||||||
|
function requireSelectedCompany(req) {
|
||||||
|
const companyId = req.user.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
|
return { error: 'No company selected' };
|
||||||
|
}
|
||||||
|
return { companyId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnErrorStatus(error) {
|
||||||
|
if (error.message === 'No company selected') return 400;
|
||||||
|
if (error.message === 'Not authorized') return 403;
|
||||||
|
if (error.message === 'Company not found') return 404;
|
||||||
|
if (error.message === 'Return not found') return 404;
|
||||||
|
if (error.message === 'Supplier not found') return 404;
|
||||||
|
if (error.message === 'No supplier selected') return 400;
|
||||||
|
if (error.message === 'Return number already exists') return 409;
|
||||||
|
if (error.message === 'Invalid supplierId') return 400;
|
||||||
|
if (error.message === 'Invalid return') return 400;
|
||||||
|
if (error.message === 'Invalid lines') return 400;
|
||||||
|
if (error.message === 'Invalid offset') return 400;
|
||||||
|
if (error.message === 'Invalid limit') return 400;
|
||||||
|
if (error.message === 'Return lines already booked on ledger') return 409;
|
||||||
|
if (error.message === 'Return in use in ledger') return 409;
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers return (supplier return) routes for the authenticated user's selected company.
|
||||||
|
* @param {import('express').Application} app
|
||||||
|
* @param {string} [basePath='']
|
||||||
|
*/
|
||||||
|
export function register(app, basePath = '') {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return:
|
||||||
|
* get:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: List returns
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: offset
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* - in: query
|
||||||
|
* name: search
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: onlyUnbooked
|
||||||
|
* schema:
|
||||||
|
* type: boolean
|
||||||
|
* - in: query
|
||||||
|
* name: supplierId
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of returns
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/return`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {};
|
||||||
|
if (req.query.offset !== undefined) opts.offset = req.query.offset;
|
||||||
|
if (req.query.limit !== undefined) opts.limit = req.query.limit;
|
||||||
|
if (typeof req.query.search === 'string') opts.search = req.query.search;
|
||||||
|
if (req.query.onlyUnbooked !== undefined) opts.onlyUnbooked = req.query.onlyUnbooked === 'true' || req.query.onlyUnbooked === '1';
|
||||||
|
if (req.query.supplierId !== undefined) opts.supplierId = req.query.supplierId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.list(req.user.id, opts);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('List returns failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return/{id}:
|
||||||
|
* get:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Get return by ID
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Return details
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/return/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ret = await db.getById(req.user.id, id);
|
||||||
|
res.status(200).json({ return: ret });
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Get return failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return:
|
||||||
|
* post:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Create return
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [date, supplierId]
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Return created
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/return`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ret = await db.create(req.user.id, req.body);
|
||||||
|
emit('return', { companyId: companyCheck.companyId });
|
||||||
|
res.status(201).json({ return: ret });
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Create return failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return/{id}:
|
||||||
|
* put:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Update return
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Return updated
|
||||||
|
*/
|
||||||
|
app.put(`${basePath}/return/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ret = await db.update(req.user.id, id, req.body);
|
||||||
|
emit('return', { companyId: companyCheck.companyId });
|
||||||
|
res.status(200).json({ return: ret });
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Update return failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return/{id}:
|
||||||
|
* delete:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Delete return
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 204:
|
||||||
|
* description: Return deleted
|
||||||
|
*/
|
||||||
|
app.delete(`${basePath}/return/:id`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.remove(req.user.id, id);
|
||||||
|
emit('return', { companyId: companyCheck.companyId });
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Delete return failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return/{id}/book:
|
||||||
|
* post:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Book return to ledger
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Return booked
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/return/:id/book`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return res.status(400).json({ error: 'id must be an integer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ret = await db.book(req.user.id, { id, warehouseId: req.body?.warehouseId });
|
||||||
|
emit('return', { companyId: companyCheck.companyId });
|
||||||
|
emit('stock', { companyId: companyCheck.companyId });
|
||||||
|
res.status(200).json({ return: ret });
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Book return failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /return/rebuild-summaries:
|
||||||
|
* post:
|
||||||
|
* tags: [Return]
|
||||||
|
* summary: Rebuild return summaries
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Summaries rebuilt
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/return/rebuild-summaries`, async (req, res) => {
|
||||||
|
const companyCheck = requireSelectedCompany(req);
|
||||||
|
if (companyCheck.error) {
|
||||||
|
return res.status(400).json({ error: companyCheck.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.rebuildSummaries(req.user.id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
const status = returnErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Rebuild return summaries failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
148
src/db/retoure.js
Normal file
148
src/db/retoure.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { getStore } from './store.js';
|
||||||
|
import { formatAttachments } from '../util/manifestAttachment.js';
|
||||||
|
|
||||||
|
function round2(num) {
|
||||||
|
return Math.round((Number(num) || 0) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateGrossTotal(lines = []) {
|
||||||
|
let total = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
const amount = Number(line.amount || 0);
|
||||||
|
const net = Number(line.net || 0);
|
||||||
|
const rebate = Number(line.rebate || 0);
|
||||||
|
const taxrate = Number(line.taxrate || 0);
|
||||||
|
const netAfterRebate = net * (1 - rebate / 100);
|
||||||
|
const gross = netAfterRebate * (1 + taxrate / 100);
|
||||||
|
total += amount * gross;
|
||||||
|
}
|
||||||
|
return round2(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePaidTotal(payments = []) {
|
||||||
|
let total = 0;
|
||||||
|
for (const payment of payments) {
|
||||||
|
total += Number(payment.amount || 0);
|
||||||
|
}
|
||||||
|
return round2(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOrderedQuantity(lines = []) {
|
||||||
|
let qty = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.shippable === false) continue;
|
||||||
|
qty += Number(line.amount || 0);
|
||||||
|
}
|
||||||
|
return Math.round(qty * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateShippedQuantity(shipments = []) {
|
||||||
|
let qty = 0;
|
||||||
|
for (const shipment of shipments) {
|
||||||
|
for (const line of shipment.lines || []) {
|
||||||
|
qty += Number(line.amount || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.round(qty * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enrichSingleRetoure(store, googleAuthId, retoure) {
|
||||||
|
if (!retoure) return retoure;
|
||||||
|
const [shippings, payments, shipments, rawFiles] = await Promise.all([
|
||||||
|
store.shipping.listByManifestId(googleAuthId, retoure.id).catch(() => []),
|
||||||
|
store.payment.listByManifestId(googleAuthId, retoure.id).catch(() => []),
|
||||||
|
store.shipment.listByOrderId(googleAuthId, retoure.id).catch(() => []),
|
||||||
|
store.manifestattachment.listByManifestId(googleAuthId, retoure.id).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const files = await formatAttachments(rawFiles);
|
||||||
|
const lines = Array.isArray(retoure.lines) ? retoure.lines : [];
|
||||||
|
const paymentsList = Array.isArray(payments) ? payments : [];
|
||||||
|
const shipmentsList = Array.isArray(shipments) ? shipments : [];
|
||||||
|
|
||||||
|
const calculatedGross = calculateGrossTotal(lines);
|
||||||
|
const calculatedPaid = calculatePaidTotal(paymentsList);
|
||||||
|
const grossTotal = retoure.grossTotal !== undefined ? retoure.grossTotal : calculatedGross;
|
||||||
|
const paidTotal = retoure.paidTotal !== undefined ? retoure.paidTotal : calculatedPaid;
|
||||||
|
|
||||||
|
const orderedQuantity = retoure.orderedQuantity !== undefined
|
||||||
|
? retoure.orderedQuantity
|
||||||
|
: calculateOrderedQuantity(lines);
|
||||||
|
const shippedQuantity = retoure.shippedQuantity !== undefined
|
||||||
|
? retoure.shippedQuantity
|
||||||
|
: calculateShippedQuantity(shipmentsList);
|
||||||
|
|
||||||
|
const isPaid = retoure.isPaid !== undefined
|
||||||
|
? retoure.isPaid
|
||||||
|
: (Math.abs(grossTotal) < 0.01 || Math.abs(paidTotal - grossTotal) < 0.01);
|
||||||
|
|
||||||
|
const isShipped = retoure.isShipped !== undefined
|
||||||
|
? retoure.isShipped
|
||||||
|
: (orderedQuantity !== 0 && Math.abs(shippedQuantity - orderedQuantity) < 0.001);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...retoure,
|
||||||
|
grossTotal,
|
||||||
|
paidTotal,
|
||||||
|
orderedQuantity,
|
||||||
|
shippedQuantity,
|
||||||
|
isPaid,
|
||||||
|
isShipped,
|
||||||
|
positions: retoure.positions ?? lines.length,
|
||||||
|
shippings: Array.isArray(shippings) ? shippings : [],
|
||||||
|
payments: paymentsList,
|
||||||
|
shipments: shipmentsList,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list(googleAuthId, opts) {
|
||||||
|
const store = await getStore();
|
||||||
|
const result = await store.retoure.list(googleAuthId, opts);
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return Promise.all(result.map((r) => enrichSingleRetoure(store, googleAuthId, r)));
|
||||||
|
}
|
||||||
|
if (result && Array.isArray(result.retoures)) {
|
||||||
|
const retoures = await Promise.all(
|
||||||
|
result.retoures.map((r) => enrichSingleRetoure(store, googleAuthId, r))
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
retoures,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rebuildSummaries(googleAuthId) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.retoure.rebuildSummaries(googleAuthId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
const retoure = await store.retoure.getById(googleAuthId, id);
|
||||||
|
return enrichSingleRetoure(store, googleAuthId, retoure);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(googleAuthId, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
const retoure = await store.retoure.create(googleAuthId, data);
|
||||||
|
return enrichSingleRetoure(store, googleAuthId, retoure);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(googleAuthId, id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
const retoure = await store.retoure.update(googleAuthId, id, data);
|
||||||
|
return enrichSingleRetoure(store, googleAuthId, retoure);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.retoure.delete(googleAuthId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShipment(googleAuthId, id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.retoure.createShipment(googleAuthId, id, data);
|
||||||
|
}
|
||||||
36
src/db/return.js
Normal file
36
src/db/return.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { getStore } from './store.js';
|
||||||
|
|
||||||
|
export async function list(googleAuthId, opts) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.list(googleAuthId, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rebuildSummaries(googleAuthId) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.rebuildSummaries(googleAuthId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.getById(googleAuthId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(googleAuthId, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.create(googleAuthId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(googleAuthId, id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.update(googleAuthId, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.delete(googleAuthId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function book(googleAuthId, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.return.book(googleAuthId, data);
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ const STORE_NAMESPACES = [
|
|||||||
'entitycontact',
|
'entitycontact',
|
||||||
'quote',
|
'quote',
|
||||||
'order',
|
'order',
|
||||||
|
'retoure',
|
||||||
|
'return',
|
||||||
'shipment',
|
'shipment',
|
||||||
'numbers',
|
'numbers',
|
||||||
'companysetting',
|
'companysetting',
|
||||||
|
|||||||
@@ -290,9 +290,18 @@ export async function importOrders({
|
|||||||
const parsedDate = dateStr ? new Date(dateStr) : new Date();
|
const parsedDate = dateStr ? new Date(dateStr) : new Date();
|
||||||
const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString();
|
const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString();
|
||||||
|
|
||||||
|
const orderTotal = Math.round(
|
||||||
|
lines.reduce(
|
||||||
|
(sum, l) => sum + (l.amount * l.net * (1 - (l.rebate || 0) / 100) * (1 + (l.taxrate || 0) / 100)),
|
||||||
|
0
|
||||||
|
) * 100
|
||||||
|
) / 100;
|
||||||
|
const isRetoure = orderTotal < 0;
|
||||||
|
const domainStore = isRetoure ? store.retoure : store.order;
|
||||||
|
|
||||||
let createdOrder = null;
|
let createdOrder = null;
|
||||||
try {
|
try {
|
||||||
createdOrder = await store.order.create(googleAuthId, {
|
createdOrder = await domainStore.create(googleAuthId, {
|
||||||
number: orderNumber,
|
number: orderNumber,
|
||||||
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
|
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
|
||||||
date: validDate,
|
date: validDate,
|
||||||
@@ -311,7 +320,7 @@ export async function importOrders({
|
|||||||
if (l.sku === offendingSku) l.sku = null;
|
if (l.sku === offendingSku) l.sku = null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
createdOrder = await store.order.create(googleAuthId, {
|
createdOrder = await domainStore.create(googleAuthId, {
|
||||||
number: orderNumber,
|
number: orderNumber,
|
||||||
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
|
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
|
||||||
date: validDate,
|
date: validDate,
|
||||||
@@ -322,10 +331,10 @@ export async function importOrders({
|
|||||||
existingOrderNumbers.add(orderNumber);
|
existingOrderNumbers.add(orderNumber);
|
||||||
ordersImported++;
|
ordersImported++;
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
log.warn(`Failed to create order ${orderNumber} after stripping parent SKU ${offendingSku}:`, retryErr.message);
|
log.warn(`Failed to create ${isRetoure ? 'retoure' : 'order'} ${orderNumber} after stripping parent SKU ${offendingSku}:`, retryErr.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(`Failed to create order ${orderNumber}:`, err.message);
|
log.warn(`Failed to create ${isRetoure ? 'retoure' : 'order'} ${orderNumber}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,29 +342,20 @@ export async function importOrders({
|
|||||||
// Handle Payment
|
// Handle Payment
|
||||||
const isPayed = String(m.payed ?? '').trim() === '1';
|
const isPayed = String(m.payed ?? '').trim() === '1';
|
||||||
const paymentType = cleanValue(m.paymentType);
|
const paymentType = cleanValue(m.paymentType);
|
||||||
if (isPayed && paymentType) {
|
if (isPayed && paymentType && orderTotal !== 0) {
|
||||||
const orderTotal = Math.round(
|
try {
|
||||||
lines.reduce(
|
const accountId = await resolveAccountId(paymentType);
|
||||||
(sum, l) => sum + (l.amount * l.net * (1 - (l.rebate || 0) / 100) * (1 + (l.taxrate || 0) / 100)),
|
if (accountId) {
|
||||||
0
|
await store.payment.create(googleAuthId, {
|
||||||
) * 100
|
accountId,
|
||||||
) / 100;
|
manifestId: createdOrder.id,
|
||||||
|
type: paymentType,
|
||||||
if (orderTotal > 0) {
|
amount: orderTotal,
|
||||||
try {
|
});
|
||||||
const accountId = await resolveAccountId(paymentType);
|
paymentsImported++;
|
||||||
if (accountId) {
|
|
||||||
await store.payment.create(googleAuthId, {
|
|
||||||
accountId,
|
|
||||||
manifestId: createdOrder.id,
|
|
||||||
type: paymentType,
|
|
||||||
amount: orderTotal,
|
|
||||||
});
|
|
||||||
paymentsImported++;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(`Failed to create payment for order ${orderNumber}:`, err.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(`Failed to create payment for ${isRetoure ? 'retoure' : 'order'} ${orderNumber}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user