262 lines
8.9 KiB
Markdown
262 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.
|