263 lines
8.9 KiB
Markdown
263 lines
8.9 KiB
Markdown
|
|
# Deliveries — n8n Signed-Copy Workflow
|
|||
|
|
|
|||
|
|
> Spec for the future n8n flow that closes out `/deliveries` items
|
|||
|
|
> by uploading the scanned signed invoice/DN to PocketBase and
|
|||
|
|
> flipping the order item to `completed`. Read this when you're
|
|||
|
|
> ready to wire the workflow — every endpoint, payload shape, and
|
|||
|
|
> field is defined below.
|
|||
|
|
|
|||
|
|
## Status: NOT YET BUILT
|
|||
|
|
|
|||
|
|
This is a **spec document**, not a working integration. The page
|
|||
|
|
`/deliveries` ships with three tabs (To Load / Back from Driver /
|
|||
|
|
Checked In); the `checked_in` items sit there until n8n lands.
|
|||
|
|
The n8n workflow itself is owned by you, not by the app — this
|
|||
|
|
doc just defines the contract so the workflow can be built
|
|||
|
|
independently.
|
|||
|
|
|
|||
|
|
## What it does
|
|||
|
|
|
|||
|
|
Once the office has typed the reference number on the signed
|
|||
|
|
paper (the `checked_in` tab in the app), the signed copy still
|
|||
|
|
needs to be:
|
|||
|
|
|
|||
|
|
1. **Scanned** — by whatever scanner / phone-camera / IMAP inbox
|
|||
|
|
the office uses today.
|
|||
|
|
2. **Uploaded** to PocketBase on the right `order_items` row.
|
|||
|
|
3. **Status flipped** to `completed` so the office can stop
|
|||
|
|
looking at it.
|
|||
|
|
|
|||
|
|
The app exposes the PB schema fields for steps 2 + 3; n8n owns
|
|||
|
|
step 1.
|
|||
|
|
|
|||
|
|
## API contract (everything the workflow needs)
|
|||
|
|
|
|||
|
|
### Inputs the workflow must know
|
|||
|
|
|
|||
|
|
The workflow runs per signed copy, not per order. A single scan
|
|||
|
|
might cover 1 line or 20. To find the right `order_items` rows
|
|||
|
|
the workflow uses the **reference number** the operator typed at
|
|||
|
|
check-in time. The contract is:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
workflow_in:
|
|||
|
|
reference: string // e.g. "INV-12345" or "DN26071401"
|
|||
|
|
file: Buffer // the scanned signed copy
|
|||
|
|
filename: string // e.g. "INV-12345-signed.pdf"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1. Find the order_items
|
|||
|
|
|
|||
|
|
Hit the PocketBase list endpoint with the reference filter:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/collections/order_items/records?filter=delivery_reference%20%3D%20%22INV-12345%22&perPage=200
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
URL-decoded: `filter=delivery_reference = "INV-12345"`
|
|||
|
|
|
|||
|
|
> PB's `~` is substring (case-insensitive); `=` is exact. Use
|
|||
|
|
> exact — the reference is meant to be exact, and a fuzzy match
|
|||
|
|
> risks pulling in the wrong customer's rows. If the workflow
|
|||
|
|
> has only a partial reference (e.g. the scanner OCR'd "INV-1234"
|
|||
|
|
> with the last digit missing), it should fail loudly rather than
|
|||
|
|
> guess.
|
|||
|
|
|
|||
|
|
Response shape (each item):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "abc123def456ghi",
|
|||
|
|
"delivery_reference": "INV-12345",
|
|||
|
|
"checked_in_at": "2026-07-20T11:30:00Z",
|
|||
|
|
"checked_in_by": "user_id_here",
|
|||
|
|
"item_status": "checked_in"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> ⚠️ Only items with `item_status = "checked_in"` should be
|
|||
|
|
> auto-completed. An item that's been typo'd and lives at
|
|||
|
|
> `with_driver` or earlier in the lifecycle should be left
|
|||
|
|
> alone — the office might re-route it.
|
|||
|
|
|
|||
|
|
Optional pre-flight guard:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/collections/order_items/records?filter=delivery_reference%20%3D%20%22INV-12345%22%20%26%26%20item_status%20%3D%20%22checked_in%22
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This way the workflow only operates on rows that are
|
|||
|
|
genuinely ready to complete.
|
|||
|
|
|
|||
|
|
### 2. Upload the signed copy
|
|||
|
|
|
|||
|
|
The signed copy goes onto `order_items.order_item_attachements`
|
|||
|
|
(multi-file field, `maxSelect: 10`, accepts any MIME). This is
|
|||
|
|
the existing field for "all attachments on this line" — same
|
|||
|
|
field that delivery-note PDFs, supplier POs, etc. live on.
|
|||
|
|
No new collection, no new field.
|
|||
|
|
|
|||
|
|
Per item, do a single PATCH with the file. PocketBase's REST
|
|||
|
|
endpoint for file uploads on an existing record is:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PATCH /api/collections/order_items/records/{id}
|
|||
|
|
Content-Type: multipart/form-data
|
|||
|
|
|
|||
|
|
--boundary
|
|||
|
|
Content-Disposition: form-data; name="order_item_attachements"; filename="INV-12345-signed.pdf"
|
|||
|
|
Content-Type: application/pdf
|
|||
|
|
|
|||
|
|
<bytes>
|
|||
|
|
--boundary--
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> Naming convention: use the reference number as the filename
|
|||
|
|
> stem (`{reference}-signed.pdf` or `{.ext}` as the scanner
|
|||
|
|
> produces). The retrieval side — see "How the office sees it
|
|||
|
|
> back" below — relies on the filename pattern to find the
|
|||
|
|
> signed copy on a given item. Don't append timestamps or random
|
|||
|
|
> suffixes.
|
|||
|
|
|
|||
|
|
### 3. Flip the status to `completed`
|
|||
|
|
|
|||
|
|
Same record, same endpoint, JSON body this time:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PATCH /api/collections/order_items/records/{id}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{ "item_status": "completed" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No body auth header is needed if the workflow uses the admin
|
|||
|
|
PB token (recommended — see "Auth" below).
|
|||
|
|
|
|||
|
|
### 4. Repeat for each item in the batch
|
|||
|
|
|
|||
|
|
Loop over the items from step 1 and do steps 2 + 3 per item.
|
|||
|
|
A single scan = one batch = N items = N×(PATCH file + PATCH status)
|
|||
|
|
calls. PB doesn't have a batch endpoint; the per-item round trip
|
|||
|
|
is fine for typical batch sizes (1–20).
|
|||
|
|
|
|||
|
|
If the workflow needs to be idempotent (resume after a crash):
|
|||
|
|
check `item_status` on the response from step 3. If it's already
|
|||
|
|
`completed`, skip. If it's still `checked_in`, the PATCH probably
|
|||
|
|
failed — log + retry once.
|
|||
|
|
|
|||
|
|
## Auth
|
|||
|
|
|
|||
|
|
The workflow should use the **PocketBase superuser token**, not a
|
|||
|
|
per-user token. The PB schema's rule is
|
|||
|
|
`@request.auth.id != ''` on the order_items update, so any
|
|||
|
|
authenticated principal works, but the superuser path is the
|
|||
|
|
simplest because:
|
|||
|
|
|
|||
|
|
- It doesn't expire while the workflow is running.
|
|||
|
|
- It doesn't need refresh handling.
|
|||
|
|
- The signed copy is an automated upload, not a user action —
|
|||
|
|
per-user audit doesn't add anything.
|
|||
|
|
|
|||
|
|
To grab the token, use the same flow as the rest of the
|
|||
|
|
back-office n8n workflows: a single "Bootstrap" HTTP node at the
|
|||
|
|
top of the workflow that hits
|
|||
|
|
`POST /api/collections/_superusers/auth-with-password` with the
|
|||
|
|
admin email + password from the n8n credential store, then stores
|
|||
|
|
`token` in a workflow variable. Every subsequent HTTP node reads
|
|||
|
|
`Authorization: Bearer {{$token}}`.
|
|||
|
|
|
|||
|
|
> If your PB instance is on the v0.23+ rename (admin collection
|
|||
|
|
> is `_superusers` not `_admins`), see
|
|||
|
|
> `docs/migration-audit.md` for the endpoint confirmation.
|
|||
|
|
|
|||
|
|
## How the office sees it back
|
|||
|
|
|
|||
|
|
Once the file is uploaded, the office has two ways to find it:
|
|||
|
|
|
|||
|
|
1. **Inside the existing order detail view** (the one at
|
|||
|
|
`/orders/[id]`) — the page already reads
|
|||
|
|
`order_item_attachements`. The new file just shows up
|
|||
|
|
alongside the others.
|
|||
|
|
2. **Inside the `/deliveries` "Checked In" tab** — when v2 of
|
|||
|
|
the page ships (out of scope for v1), it'll show a "View
|
|||
|
|
signed copy" link per row that points at PB's file URL. The
|
|||
|
|
app's `api/v1/files/[collection]/[recordId]/[filename]`
|
|||
|
|
route is the canonical way to stream a PB file back through
|
|||
|
|
Next.js with auth.
|
|||
|
|
|
|||
|
|
## Filename contract
|
|||
|
|
|
|||
|
|
The workflow must use a deterministic filename per item so the
|
|||
|
|
retrieval side can find it without scanning the whole attachment
|
|||
|
|
list:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
{reference}-signed{ext}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Examples:
|
|||
|
|
|
|||
|
|
- `INV-12345-signed.pdf`
|
|||
|
|
- `INV-12345-signed.png`
|
|||
|
|
- `DN26071401-signed.pdf`
|
|||
|
|
|
|||
|
|
If the scanner produces something else, the workflow should
|
|||
|
|
**rename it** to this pattern before the upload — not rely on
|
|||
|
|
whatever the device names it. The reference is the only stable
|
|||
|
|
key; everything else is mutable.
|
|||
|
|
|
|||
|
|
## Failures to handle
|
|||
|
|
|
|||
|
|
The workflow should treat these as "log + retry" rather than
|
|||
|
|
"throw and stop the whole run":
|
|||
|
|
|
|||
|
|
- **0 items found for the reference** — typo'd reference or a
|
|||
|
|
paper that was never typed into the office. Don't create a new
|
|||
|
|
item; surface the unmatched reference to a "needs review" sink
|
|||
|
|
(a separate `logs` collection or a Slack message — your call).
|
|||
|
|
- **HTTP 5xx on the PATCH** — PB blip. Retry once with a 2s backoff.
|
|||
|
|
- **`item_status` came back as `cancelled`** — the office
|
|||
|
|
cancelled the item in the meantime. Skip the upload + status
|
|||
|
|
flip; surface to "needs review".
|
|||
|
|
|
|||
|
|
The workflow should treat these as **hard failures** that abort
|
|||
|
|
the run:
|
|||
|
|
|
|||
|
|
- **More than 50 items for one reference** — almost certainly a
|
|||
|
|
data bug. Don't auto-upload 200 files based on a single scan.
|
|||
|
|
- **Auth token expired mid-run** — re-bootstrap from the start.
|
|||
|
|
|
|||
|
|
## Suggested n8n shape
|
|||
|
|
|
|||
|
|
If you want a starting point:
|
|||
|
|
|
|||
|
|
1. **Trigger**: depends on the scanner. Common patterns:
|
|||
|
|
- Watch a dedicated IMAP folder (signed copies get emailed
|
|||
|
|
from the scanner).
|
|||
|
|
- Watch a folder (e.g. `/shared/scanner/` on a Synology).
|
|||
|
|
- A webhook the office hits from a phone app.
|
|||
|
|
2. **OCR / extract reference**: if the scan is an image, OCR
|
|||
|
|
it (Tesseract via a `Execute Command` node, or one of the
|
|||
|
|
hosted OCR services) to read the reference number from the
|
|||
|
|
paper. Cross-check against the manually-typed reference from
|
|||
|
|
the office — if they disagree, route to "needs review".
|
|||
|
|
3. **HTTP Bootstrap**: get the PB superuser token.
|
|||
|
|
4. **HTTP Find items**: list by reference (step 1 above).
|
|||
|
|
5. **Loop per item**: upload + flip (steps 2 + 3 above).
|
|||
|
|
6. **Log + cleanup**: move the source file out of the watch
|
|||
|
|
folder, log success/failure.
|
|||
|
|
|
|||
|
|
## Out of scope for the app
|
|||
|
|
|
|||
|
|
These are deliberately **not** part of the `/deliveries` page
|
|||
|
|
v1 and don't need any app change to support:
|
|||
|
|
|
|||
|
|
- The actual scan ingestion (n8n).
|
|||
|
|
- The OCR (n8n).
|
|||
|
|
- The signed-copy viewer in the page (a future "Completed" tab
|
|||
|
|
once there's enough data to be worth showing).
|
|||
|
|
- A "Completed" tab on the page. When the workflow lands and
|
|||
|
|
`item_status = "completed"` rows accumulate, we can add a 4th
|
|||
|
|
tab. The schema already supports it — the page just doesn't
|
|||
|
|
render it yet.
|