Reminders: qty default 1 (never ask). After successful notify_admin.py → silence until order result. Fresh customer data every order.
python skills/reset_orders_clients_db.py
Truncates orders and clients (and stock_reservations if that table exists); restarts ID sequences. Does not modify inventory or categories.
After upgrading the repo, create the table once (PostgreSQL):
psql "$DATABASE_URL" -f schema_stock_reservations.sql
(On Windows PowerShell pass your connection string from .env.)
When a customer order is waiting in data/pending_orders.json, manage_stock.py uses sellable units = on-hand quantity minus rows in stock_reservations so another customer is not told "in stock" for units already tied to pending Telegram approvals. Reservations are removed when the order is approved (sale logged) or declined (dispatch_pending_orders.py decline).
When physical or sellable quantity crosses into low stock (≤3) or a single remaining unit (=1), skills/stock_alert.py sends one Telegram (same bot/chat as notify_admin.py). Each message always shows both metrics: Fizikisht: before → after and E shitshme: before → after.
Triggered after: log_order_internal.py (approved orders), manage_stock.py deduct, and dashboard ± quantity (adjust_inventory_quantity in ysk_db/repository.py).
python skills/manage_stock.py --sku "T100" --action check
python skills/manage_stock.py --sku "T100" --action check --requested-qty 2
python skills/manage_stock.py --sku "M-06" --action check --json
Output includes product_name, PRICE_EUR, DESCRIPTION_JSON, and full DESCRIPTION_MD (when set in DB). That block is the only allowed source for price, stock wording, dimensions, drawers, and specs for that SKU — see Tool truth, Invariant — never "sell" the photo's numbers, and SKU match but flyer / photo disagrees in instructions.md. Never invent stock counts — tool is IN/OUT only.
Customer sent a photo/screenshot of a flyer or Facebook post? Resolve SKU, then run --json for that SKU before quoting any € or cm to the customer — do not treat vision/OCR as the offer. Do not send any partial old-vs-new sentence before that tool returns.
For one atomic customer reply after the stock check, prefer:
python skills/render_product_reply.py --sku "M-06"
python skills/render_product_reply.py --sku "M-06" --old-vs-new
render_product_reply.py uses manage_stock.py as the official source and prints one final Albanian reply. If the underlying tool step fails, it prints only Nje moment. so you do not concatenate the old-vs-new intro with a tool failure.
Text-only product interest with no SKU/image is catalog help only for that turn. Do not send catalog help and then a second product-spec message for the same customer message.
When vision or pasted text mentions a product but the SKU is unclear, resolve against DB + manifest.json:
python skills/resolve_sku_from_text.py --blob "FORES-106 dollap 89 EUR"
# or: python skills/resolve_sku_from_text.py --blob-file ocr.txt
Use suggested_sku with manage_stock.py when ambiguous is false; if ambiguous, ask the customer (in Albanian) to confirm the exact model/SKU.
Optional alias support:
data/sku_aliases.json, resolve_sku_from_text.py will score those aliases toward the mapped SKU.data/sku_aliases.example.json.For unresolved product-ish text, route safely with:
python skills/route_sales_inquiry.py --text "Meto model a e keni"
If the result is catalog_protocol, send the Facebook/Instagram catalog reply. Do not use the off-topic refusal for unresolved model names.
When the customer already sent a code earlier, but the newest message is vague after Nje moment. (for example ?, po, edhe?), recover the active SKU from recent chat before using catalog defense:
python skills/recover_active_sku.py --blob "Customer: M-06\nAgent: Nje moment.\nCustomer: ?"
If suggested_sku is present and ambiguous is false, continue with:
python skills/manage_stock.py --sku "M-06" --action check --json
python skills/render_product_reply.py --sku "M-06"
Do not ask for the code or photo again when recent chat already contains a recoverable SKU.
Before sending a final WhatsApp reply (especially after vision/English drafts), vet the text:
python skills/guard_customer_message.py --text "Your draft reply here"
python skills/guard_customer_message.py --text "English draft..." --ensure-albanian
Stdout is the message body to send (safe replacement if moderation fails). Requires OPENAI_API_KEY env var. Optional env: OPENAI_GUARD_MODEL (default gpt-4o-mini) for --ensure-albanian.
Do not paste raw English vision captions to the customer — use --ensure-albanian or rewrite manually in Albanian first, then guard.
Single SKU:
python skills/notify_admin.py --type approval \
--customer-session "agent:main:whatsapp:direct:+3830000000000" \
--sku "T100" --description "..." --qty 1 \
--product-total "75" --postage "3" --grand-total "78" \
--first-name "..." --last-name "..." \
--phone "..." --address "..." --municipality "..."
Multiple SKUs in one cart (required when the customer orders more than one product code in the same submission): one Telegram message, multiple Porosi IDs, all lines recorded on approve. line_total = that row's product subtotal (EUR); postage is 3 × sum(qty) inside the script.
python skills/notify_admin.py --type approval \
--customer-session "agent:main:whatsapp:direct:+3830000000000" \
--items-json "[{\"sku\":\"M-06\",\"description\":\"TRIO\",\"qty\":1,\"line_total\":\"79\"},{\"sku\":\"FORES-106\",\"description\":\"FORES\",\"qty\":1,\"line_total\":\"89\"}]" \
--first-name "..." --last-name "..." \
--phone "..." --address "..." --municipality "..."
On Windows, if quoting JSON is painful, copy data/cart_items.example.json, edit SKUs/prices, save as e.g. data/cart_items.json, then:
python skills/notify_admin.py --type approval \
--customer-session "agent:main:whatsapp:direct:+3830000000000" \
--items-json "data/cart_items.json" \
--first-name "..." --last-name "..." \
--phone "..." --address "..." --municipality "..."
(--items-json accepts either inline JSON or a path to a .json file.)
Escalation: --type escalation + --message "..."
Never run log_order_internal.py.
notify_admin.py now fails fast when any required customer field is missing (first-name, last-name, phone, address, municipality). Ask for missing fields first; do not use the tool as a validator.
For discount requests, use a fixed text reply and no tool call:
Cmimi është fiks, nuk mundemi me bo zbritje.
Optional deterministic router:
python skills/route_sales_inquiry.py --text "Pak shtrejt po m doket a muni me ma bo naj cmim ma t mire"
This should return fixed_price_reply.
Check data/product_photos/manifest.json or python skills/list_product_photos.py. Use send_product_photo.py only when the customer explicitly asked to see photos of that product (not for routine stock/price replies).
python skills/send_product_photo.py --sku "K2737" --confirm-visual-request \
--customer-session "agent:main:whatsapp:direct:+3830000000000"
# optional: --index 1 | --send-ack-before-photo (usually omit; default is quiet send)
Preflight: --confirm-visual-request is required (proves policy: customer explicitly asked to see the product). Without it the script exits 3 with PREFLIGHT_FAILED. Owner-only bypass for tests: YSK_SKIP_PHOTO_PREFLIGHT=1.
On PHOTO_DELIVERED: empty assistant message. Do not send images any other way.
Order fields for notify_admin.py: --first-name, --phone, etc. must come from text the customer sent, not from WhatsApp profile or session id.
.\Start-Gateway.ps1 (writes debug logs; default port 18789). .\health-check.ps1 — gateway TCP, PostgreSQL (DATABASE_URL), WhatsApp creds.json, openclaw doctor..\venv\Scripts\streamlit run dashboard\app.py — inventory/orders/clients UI; see README-dashboard.md for secrets and .env./reset, !stop, or similar — recovery is covered in prompts (instructions.md safety response + NO_REPLY rules). openclaw doctor --fix when appropriate). That is outside the sales agent's chat tools. NO_REPLY for anything that looks like business intent.