A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.
What is here:
storefront/ the Next.js app
backend/ ONLY the custom Spree code. Spree itself is a prebuilt image
that mounts no app code, so every file here needs an explicit
volume entry in docker-compose.yml or it is silently ignored.
catalogue/ the scripts that loaded and maintain the products
docs/ manifest, mock registry, napkins
Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.
The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
import json, time, requests
|
|
from pathlib import Path
|
|
|
|
import os as _os
|
|
|
|
|
|
def _spree_admin_key() -> str:
|
|
"""The Spree admin API key. Required — never defaulted."""
|
|
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
|
|
if not key:
|
|
raise SystemExit(
|
|
"SPREE_ADMIN_KEY is not set.\n"
|
|
" export SPREE_ADMIN_KEY=<the admin API key>\n"
|
|
"It is deliberately not stored in this file — see README.md."
|
|
)
|
|
return key
|
|
|
|
|
|
API = "https://api.jux-lab.com/api/v3/admin"
|
|
H = {"X-Spree-Api-Key": _spree_admin_key(), "Content-Type": "application/json"}
|
|
PRODUCTS = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products")
|
|
MYR_TO_USD = 4.8
|
|
|
|
prices = {}
|
|
for folder in PRODUCTS.iterdir():
|
|
info = folder / "info.json"
|
|
if info.exists():
|
|
prices[folder.name] = round(float(json.loads(info.read_text())["price_myr"]) * MYR_TO_USD, 2)
|
|
|
|
items, page = [], 1
|
|
while True:
|
|
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
|
|
items += d.get("data", [])
|
|
if page >= d.get("meta", {}).get("pages", 1):
|
|
break
|
|
page += 1
|
|
|
|
print(f"{len(items)} products, {len(prices)} source prices")
|
|
ok = fail = 0
|
|
for i, p in enumerate(items):
|
|
amt = prices.get(p["slug"])
|
|
if amt is None:
|
|
print(f" no source price: {p['slug'][:45]}")
|
|
fail += 1
|
|
continue
|
|
r = requests.patch(f"{API}/products/{p['id']}", headers=H,
|
|
json={"prices": [{"amount": amt, "currency": "USD"}]}, timeout=60).json()
|
|
if "error" in r:
|
|
fail += 1
|
|
print(f" FAIL {p['slug'][:40]}: {r['error'].get('message')}")
|
|
else:
|
|
ok += 1
|
|
if (i + 1) % 40 == 0:
|
|
print(f" {i+1}/{len(items)}")
|
|
time.sleep(0.12)
|
|
|
|
print(f"Done ok={ok} fail={fail}")
|