# AGENT.txt: turn on a working shopping cart for a DW Alliance tenant Updated 2026-11-14. Reference tenant: mingapothecary.com. Read ENDPOINTS.txt (what exists) and STATUS.txt (what works and what does not) first. ## Rules you must keep 1. Use only the app interface: /session/, /files/, /store/…, /address/, /crm/, /relation/, /store/settings/, /store/inventory/. Never write to the database. Read-only checks are fine. 2. Secrets: - Never print, log, commit or put in a page: session ids, brand codes, passwords or payment secrets. - Keep the staff session in an environment variable and the brand code in a local file outside any web root. - Mask both in every log line. - Session ids arrive by email only. 3. Paths: - Always use trailing slashes on API paths. - Never name a page after an API folder. Use /store/cart.html, never /store/cart/. - Also avoid /store/settings/, /store/inventory/ and /store/kit/ as page paths (those are app folders). 4. Copy: - Don't invent features. The payment link on checkout is not on yet, so the page must say no payment is taken automatically. - Follow the owner's own wording rules for public copy (ask them for the list before writing any page text). 5. Release steps: - Preview locally first and show the owner screenshots. - Publish only after the owner's explicit yes. - Keep a one-step rollback. - Track every change in a GitHub issue in jbrake1/dwalliance, with commits that reference it. ## 0. Prerequisites (check, don't assume) | Check | How | |---|---| | The domain is claimed and served | curl -sI https://DOMAIN/ → 200. POST https://DOMAIN/store/products/ {"action":"list"} → {"ok":true,…} | | You have a staff session for the brand | POST https://app.dwalliance.com/session/ {"email":"OWNER_EMAIL","brand":"BRAND_CODE"} → {ok, mailed}. The id is emailed to the owner, who gives it to you through a secret-entry form, never in chat. | | The session has rights | SUPER_ADMIN, or STORE CREATE/UPDATE plus FILE CREATE/UPDATE. CRM rights are needed for staff /address/. | | You know the selling company | company_id of the brand's company. See CRM.txt. | ## 1. Fill in the tenant config Copy tenant.example.json to tenant.json and fill in the brand name, canonical host, company_id, contact email, categories, products, image files and stock. No secrets go in it. ## 2. Store settings (shipping, tax, order-copy email) Set these once per selling company, before taking orders: POST https://app.dwalliance.com/store/settings/ {"action":"update","session":"$SESSION","brand":"$BRAND","company_id":"COMPANY_ID", "shipping_flat":"8.00","free_shipping_over":"75.00","hi_get_rate":"4.712","tax_shipping":false, "store_email":"store@example.com"} - shipping_flat is the flat shipping rate. - free_shipping_over is the optional subtotal at or above which shipping is free. Send null to use the default (or to turn the threshold off if the default has none). - hi_get_rate is Hawaii GET as a percent. It applies only when the shipping address is in Hawaii. - tax_shipping is whether GET is also charged on shipping (default is no). - store_email is where the store's copy of each new order is sent. Read the effective settings with {"action":"read","company_id":"COMPANY_ID"}. ## 3. Create the categories (staff, once per category) POST https://app.dwalliance.com/store/product/categories/ {"action":"create","session":"$SESSION","brand":"$BRAND","company_id":"COMPANY_ID","name":"Jing","position":0, "description":{"anchor":"jing","title":"The Jing Collection","subtitle":"Essence","summary":"…"}} → {"ok":true,"category_id":"…"} Save each category_id against its key in a local, uncommitted state file. ## 4. Upload the images through /files POST https://app.dwalliance.com/files/ {"action":"create","session":"$SESSION","file_name":"five-treasures.jpg","file_path":"store/products", "file_type":"JPG","public_private":"PUBLIC","file_data":""} → {"ok":true,"id":"FILE_ID"} - public_private defaults to PRIVATE, and a private image never shows. Always send PUBLIC. - Strip EXIF and GPS first, for example exiftool -all= file.jpg or a Pillow re-save. Never upload originals that contain location data. - Keep images at 1600 px or less on the long side. The upload limit is about 12 MB. ## 5. Create the products POST https://app.dwalliance.com/store/product/ {"action":"create","session":"$SESSION","brand":"$BRAND","company_id":"COMPANY_ID","name":"Five Treasures", "price":"34.00","variant_name":"Default","stock":12,"active":true,"description":"

…

"} → {"ok":true,"product_id":"…","slug":"five-treasures","variant_id":"…","description_id":"…"} This one call makes the product, its company claim, the default variant and the main description. If you pass stock, it becomes the opening inventory at the store's default location ("Main" is created when first needed). After that, change stock through /store/inventory/, not by editing the variant directly. Leave stock out to not track stock. Then: - Attach images: POST /store/product/images/ {"action":"create","id_type":"PRODUCT","associated_id":"PRODUCT_ID","file_id":"FILE_ID","position":0, "description":{"src":"/store/products/five-treasures.jpg","alt":"Five Treasures jar"}} The src and alt convention lets pages show images without an extra lookup; see STATUS G6. - Link categories: POST /store/product/categories/ {"action":"link","category_id":"…","product_id":"…"} - Extra variants: POST /store/product/variants/ {"action":"create","product_id":"…","name":"Large tin","price":"34.00","stock":5,"position":1} Verify with no session on the tenant domain: products list and read, and categories list. ## 6. Inventory (receive, adjust, check, rebuild) Stock is tracked per variant and location. The movement history is the source of truth and is never overwritten. A sale movement is written at checkout. POST https://app.dwalliance.com/store/inventory/ {"action":"receive","session":"$SESSION","brand":"$BRAND","variant_id":"…","quantity":12, "unit_cost":"4.25","notes":"Opening delivery"} - receive: add stock (quantity 1+). - adjust: a signed quantity, or counted (a physical count; the difference is recorded). Refused if it would take a location below 0. - stock: on hand, reserved and available (on hand minus reserved), per location and in total. - rebuild: recomputes balances from the movement history and reports any differences it fixed. List locations with /store/inventory/locations/ (list, create, update). List movements and balances with /store/inventory/movements/ and /store/inventory/balances/. A product can't be sold past what is available. Do not use variant update to change tracked stock once inventory is in use. ## 7. Build and preview the pages python3 tools/build.py tenant.json out/ # read-only: public API + /files read + the brand's own header/footer DW_PREVIEW_DEMO_CART=1 python3 tools/preview_server.py out/ https://DOMAIN 8798 # writes blocked; demo cart only Check /store/, a product page, /store/cart.html, /store/checkout.html and /store/account.html on desktop and on 390 px mobile. Take screenshots and show the owner. Wait for the owner's explicit yes before going further. Files to publish: - store/index.html - store/.html - store/cart.html, store/checkout.html, store/account.html (all noindex) - assets/js/store.js, assets/js/cart.js - assets/css/store-kit.css The kit templates already show a shipping and tax quote, send customers to email-code sign-in, include delete account on the account page, and say that no payment is taken automatically. ## 8. Publish through /files and wire the nav 1. Back up first. Search the brand's files, read every page you'll change, and save its bytes, md5 and file id to a local manifest outside any web root. 2. New files: /files/ create with public_private:"PUBLIC", the right file_path (store, assets/js or assets/css) and file_name. 3. Existing files: read the live bytes, confirm their md5 still matches your backup (if not, someone changed it, so stop and ask), then update with the same id. Never create a second row with the same path and name. 4. Nav: - In every site page, point the Shop or Store link at /store/ (with the slash). - Add Cart to the header if the owner wants a cart badge. - Add on pages that show the badge. 5. Rollback script: for updated files, update back to the backup bytes. For created files, delete by id. tools/publish.py does the backup, publish and rollback (--apply, --rollback); run it without flags first for a dry run. 6. Caches: anonymous page cache lasts about 5 minutes, and the sitemap rebuilds itself after any /files change. ## 9. Customer sign-up, sign-in, addresses and delete account These work on any claimed domain. checkout.html and account.html already drive them: - New customer: /end_user_account_setup/ request_code → verify (signed in) → profile (names, phone). - Returning customer: /customer_session/ request_code → verify. - Addresses: /address/ create and list. Checkout picks one shipping address, and billing defaults to it. - Delete account: /end_user_account_setup/ delete_account with confirm_email (the customer's own email). This removes sign-in, phone numbers, saved addresses and open carts (history rows are kept), ends every session on this store, and signs them out. Orders stay in the store's records. It is refused while an order is waiting for payment; the store must cancel that order first. Tell the owner: - Sign-up only works from US networks without a VPN. - Sign-in on www. and on the bare domain are separate (STATUS G10). - Codes only go to real inboxes. ## 10. Payments: what to do today - Today: payment_mode:"offline". Checkout computes shipping and tax, emails a confirmation to the customer and a copy to store_email, and writes a sale movement. No charge is made automatically. Each store can connect its own PayPal account, but the payment link on checkout is not switched on yet. - The owner then: 1. Lists new orders: POST /store/invoice/ {"action":"list","session","brand","status":"PENDING"}. 2. Optionally adjusts shipping or tax on a PENDING invoice: {"action":"update","invoice_id","shipping":"8.00","tax":"1.50"} (the total is recalculated). Usually the checkout figures are already right. 3. Collects the money outside the site, for example a PayPal invoice or a phone call. 4. Records it: POST /store/invoice/payment/ {"action":"create","invoice_id","amount":"…","method":"PAYPAL","provider_ref":"PAYPAL_TXN_ID"}. The invoice becomes PAID. 5. Ships: POST /store/shipment/ {"action":"create","invoice_id","items":[{"invoice_product_id","quantity"}],"carrier":"USPS","tracking_number":"…","status":"SHIPPED"}. - Optional: record_pending_payment:true with payment_method:"PAYPAL" makes checkout also record a PENDING payment row. No charge is made. - Online charging: paypal_start and paypal_capture exist but answer 501 until the platform flag and the store's PayPal flag are both on. Don't add PayPal or Stripe buttons that bypass the invoice. ## 11. Test an order (needs the owner's OK; it creates real rows) Use the canonical host, a real inbox you can read, and a product with available stock (or a hidden test product with active:true, then receive 1). 1. Add to cart, change the quantity and remove a line on cart.html. Each answer has ok:true. 2. Check out → "I am new here" → email → code → name → address. Confirm the quote (subtotal, shipping, tax, total) then Place order. The confirmation shows the total, and a copy is emailed to the customer and the store. 3. account.html lists the order as PENDING. 4. Staff: invoice list shows it. A COMPLETED payment for the balance turns it PAID. Create a shipment. 5. Clean up through the app: invoice update status:"VOIDED". VOIDED does not automatically restore stock; put it back with /store/inventory/ adjust if needed. To remove the test customer, use delete_account from that customer's session (orders are kept). 6. Write the results into the GitHub issue. ## 12. Verify Anyone: run tools/verify_public.sh DOMAIN out/ (HTTPS, apex and www, md5 per file, noindex on cart, checkout and account). Also check apex and www over HTTPS from outside, and the mobile layout. Then close the GitHub issue with the results. See CHECKLIST.txt for the tick list and CRM.txt for customers and accounts.