A Swiss job-seeker uploads selfies, pays CHF 49 once with no account, and gets a set of professional Bewerbungsfotos emailed back. That's the whole product. Everything that isn't "generate headshots" — login, payment, email — belongs to Core. foto-service is ~the product logic and three HTTP calls.
It's the proof that the locked architecture pays off: a new revenue product is a small, disposable service, not another monolith. The LoRA generation pipeline already exists and works (13+ models trained) in the wardrobai codebase; foto-service is the thin Core-client shell around it.
Identity is the order, not a user. foto's own job_id rides in the success_url it controls, and Core appends the Stripe session_id — so the return hits /fulfill?job_id=…&session_id=… with both. No account, no JWT, no plan to flip, no fragile URL-parsing. The product trusts only a server-side guest-verify-session, never the browser.
| File | Pattern / skill |
|---|---|
core_client.py | Sole HTTP boundary to Core — module-level create_checkout/verify_session/send_email, one auth-header helper, explicit timeout, raise_for_status, narrowed typed return (http-service-adapter) |
replicate_client.py | Sole boundary to Replicate — returns list[str] URLs only, never raw SDK objects (http-service-adapter) |
service.py | verify → generate → email, idempotent on session_id (guest-checkout-grant + inbound-webhook-handler discipline) |
jobs.py | On-disk SQLite order store (Neon is Core-only per the data decision) (local-first-sqlite-store) |
errors.py | {code, message, request_id} envelope mirroring Core (error-envelope) |
routes.py / dtos.py | Thin DTO-validated blueprint (the house reference template) |
openapi.yaml | Spec slice for the foto endpoints (contract-first-api) |
app.py / Dockerfile | Boot env-gate (fail fast), 12-factor $PORT, non-root, HEALTHCHECK over 127.0.0.1 (dependency-health-probe) |
tests/ | boot-gate · checkout persists job · idempotent fulfill · unpaid does no work · health — Core + Replicate mocked. 12/12 green. |
1. Billing auth-wall → solved. PR #21 adds guest-checkout-session + guest-verify-session (no auth, no ownership gate; verify returns paid, email, amount_total, metadata). foto is already wired to them — checkout + payment verification work with no account.
2. Order-confirmation email → solved. PR #21 sends the guest order-confirmation from Core's webhook (service layer, sidestepping the HTTP route's recipient scoping). foto no longer sends that one.
The headshots are generated after payment, so Core's webhook can't carry them. foto's results-ready email still hits Core's authed, recipient-scoped /email/send, which 403s for a guest address. The code sends it, logs the failure, and still marks the job delivered (the /fulfill response returns image_urls so the frontend can show results directly). Fix = the PR #21 trusted-service bypass on /email/send, OR a foto-owned Resend sender. Your decision.
Tests mock Core + Replicate, so they make no network calls and need no secrets.
wardrobai/server./data volume are TODO.claim_for_generation) prevents double Replicate spend across the 2 gunicorn workers. ✓ (was the review's top finding)wardrobai, zero trendfy references on the headshot product.