# Demo instance tooling (VM 118 `musterlager-demo`, https://edge.logship.de)

Scripts that fill a freshly cloned LogShip instance with demo data **through the app's own
server routes** (sign-in → cookie jar → `/api/...`), so every record looks like one created in
the UI. All are idempotent (lookup by name / search key first) and can be re-run after a failure.
Run them on the instance itself (`BASE=http://127.0.0.1:3001`) with the `demo` admin login from
`/root/demo-credentials.txt`.

| Script | Purpose |
|--------|---------|
| `seed-demo-data.sh` | generic demo set: merchant "Muster Shop GmbH" (org, warehouse, bins, merchant user), 10 products with prices, opening stock, 6 consignees, 6 sales orders in mixed states, 3 shipments, 1 invoice |
| `import-merchant-catalog.sh` | onboard a merchant (lead) and import a catalog JSON with images (Strapi), net/gross prices, master data (SKU = search key, UPC, weight/dims, `group1` = brand, `group2` = reference, `classification` = kind) and opening stock; `LIMIT=n` for a test run |
| `seed-orders-returns.sh` | ~100 sales orders for a merchant in mixed states (draft, prepared, completed/picking, shipped, invoiced, voided), 1–5 lines, qty 1–3, dates spread over 30 days, consignees from an anonymised JSON across DE/AT/IT/FR/UK/ES/NL/CH/BE/LU; shipments per customer, invoices, RETURNS (completed RMA + customer return receipt into bin `RET-01-01`) and demo photos (`make-demo-photo.py`) attached to orders, RMAs and return receipts through the app's Strapi attachment model |
| `build-consignees.py` | anonymised consignee list from two production samples (names sampled in random order, addresses): first/last names, street names, house numbers and postal/city/country triples are recombined independently, so no row is a real person or address; 21 European countries (built-in city/postal/street pools for countries without a sample); raw samples deleted after use. Output committed as `kazaar/consignees.json` (200) |
| `rename-consignees.sh` | re-anonymise existing consignees in place (name/e-mail/address via REST), so orders referencing them show the new identity |
| `make-demo-photo.py` | renders a photo-like JPEG (product image on a table, caption bar, camera stamp; `--parcel` draws a return parcel with label) for attachments |
| `hess/build-catalog.py` | catalog for the lead Hess (Livipur Kinder. Spiel. Versand GmbH, livipur.de — plentymarkets): fetches `/rest/io/variations?itemsPerPage=500&page=N` (unauthenticated, 1 227 variations / 916 items) + `/rest/io/categorytree`, writes `catalog.json` (value = shop article number, brand = manufacturer in `group1`, category path in `group2`, top category in `classification`, in-store EAN-13 `20…` from the variation id, estimated weight/dims, stock 0 for unsalable articles) and `masters.json` (one master per item with 2+ variations, `parentValue` on the variations) |
| `create-master-articles.sh` | master ("summary") articles from `masters.json`: quick-store (name + image, no prices) → products/update → `[id]/flags` (`isSummary` true, `isStocked/isSold/isPurchased` false) |
| `set-parent-ids.sh` | writes a catalog field (`KEY`, e.g. `parentValue` or `shopItemId`) into `M_Product.ParentId` of every article of an org (REST, idempotent) — the variant ↔ master link the "Varianten" card reads |
| `add-purchase-prices.sh` | purchase price list version (Einkauf 1000004) for every article with a gross price: `gross / VAT × MARGIN` (Kazaar: margin 1, Hess: 0.55) |
| `kazaar/build-catalog.py` | builds the catalog JSON for the lead Kazaar Fragrances from the storefront GraphQL feed (`catalog-raw.json`, 3 pages of `Products(collection_slug:"alle-produkte")`): every fragrance as 30/50 ml × 20/30/50 % (843 articles), oils with their real 3/5/8 ml, car scents/refills/bundles skipped |

Catalog entry shape: `value, name, grossPrice, netPrice, upc, sku, weight, length, width, height,
group1, group2, classification, description, documentNote, imageUrl, stock`. **Brand** lives in
`M_Product.Group1` until a real brand entity exists in iDempiere; the "inspired by" reference in
`Group2`; the shop's own variant SKU in `SKU`, barcode in `UPC`.

Gotchas learned: never put `+` into a `/api/filters/...` OData filter (`MovementType eq 'C+'` arrives as `'C '` → no match → duplicate documents on re-runs; filter by doc type instead); `rma/store` needs `salesRepId` and `AD_Org_ID` on each `rmaLines` entry (nested lines are passed through verbatim); `attachments/[table]/[id]/files` wants the Strapi **documentId**; Strapi filter URLs need `curl -g`; `partners/partners/store` needs `languageId`; `orders/store` needs
`paymentTermId` + `docStatusId:"DR"`; iDempiere REST wants `POReference` casing; the storefront
blocks `/products.json` but its own `/api/graphql` answers unauthenticated (schema: `Products`,
`Product`, `Variation`, `Vendor`); the clone needed `05-apply-env-overrides.sh` first, otherwise
routes using `config.api.idempieretoken` answer 401 after the REST secret rotation.

Sequence gotcha: `quick-store` creates products with `value: ''`, so iDempiere draws the search key
from the client's `DocumentNo_M_Product` sequence — on a template clone that sequence (1000199…)
collides with the legacy billing products whose Value is numeric → "eindeutige Daten erforderlich"
after a few hundred creates. Bump it first:
`update adempiere.ad_sequence set currentnext=5000000 where name='DocumentNo_M_Product' and currentnext<5000000`.

**Variant model (2026-10-26, Hess):** no schema change — the master article is a normal `M_Product`
with `IsSummary = Y` and `IsStocked/IsSold/IsPurchased = N` (pure structure entity, never stocked,
picked, ordered or priced); each variation carries the master's search key in `M_Product.ParentId`
(varchar 30, otherwise unused; `shopify_parent_product_id` belongs to the Shopify sync and stays
untouched). Frontend: `GET /api/materials/products/[id]/variants` → `ProductVariantsCard` on the
product edit page + "Variante von" column on the products list. Order for a merchant: import catalog →
`create-master-articles.sh` → `set-parent-ids.sh KEY=parentValue` → `add-purchase-prices.sh`.
Hess on the demo: org 1000018 / warehouse 1000018 / partner 1045794, 60 bins HE-01-01…HE-12-05,
1 227 articles + 114 masters, user `hess` (password in `/root/demo-credentials.txt`).
