Port Print Forge shopper accounts, slim contact form, purchase reviews, Stripe-hosted cards, and shipment tracking into this template. These belong to catalog features, not the always-on CRM portal:
Capability
Feature flag / app
Customer register/login/profile, order history, reviews, Stripe customer ids
FEATURE_SHOP → shop (+ accounts customer URLs only when shop is installed)
Slim public contact form (email + interest + message)
always-on public, but keep CRM Contact name/phone/address for portal import
Do not copy Print Forge branding, copy, SKUs, or demo addresses. Keep template-neutral labels ("Shop", "Account", generic product names in seed).
Reference implementation: print_forge#7 / print_forge#8 (feature/7-customer-accounts-reviews). Print Forge also ships seed_demo; this template currently has noseed_demo command — port it and extend it (see §9).
Why
Print Forge added a guest-checkout shop with staff-only /accounts/login/. Buyers had no way to see orders, save a shipping address, or review products. Cards must stay on Stripe. Shipping labels already go through EasyPost (and Pirate Ship CSV); customers need tracking feedback without a second Shippo integration.
1. Slim public contact form (always-on public)
Current template (site/public/forms.py): first_name, last_name, email, phone, full address, interest, message.
Staff portal login stays at /accounts/login/ (RealtorProfile). Shoppers are non-staff Django Users.
Model
accounts.CustomerProfile (OneToOne User):
phone
shipping_address JSON (same shape as Contact.make_postal_address)
stripe_customer_id (blank OK; never store PAN/CVC/payment method numbers)
URLs — only include when apps.is_installed("shop")
Mount /account/ from accounts.customer_urls in the project urls.pyinside the existing if apps.is_installed("shop") block (do not always-on include it).
site/print_forge/urls.py → equivalent in template project urls: shop-gated include
3. Shopper password reset (required; not optional)
Print Forge register/login shipped without reset. This template port must include it so buyers are not locked out.
Use Django auth_views.PasswordResetView / PasswordResetDoneView / PasswordResetConfirmView / PasswordResetCompleteView under /account/, public base.html templates, email looking up User.email (same as username).
Do not add Shippo as a second label buyer. EasyPost remains the shipping API. Pirate Ship stays CSV export. Tracking lookup uses EasyPost Tracker API, which works for labels bought in EasyPost and tracking numbers pasted from Pirate Ship / the carrier (USPS, UPS, FedEx, DHL).
CSRF-exempt POST /portal/shipping/webhooks/easypost/ (EASYPOST_WEBHOOK_SECRET optional header/?token=)
Customer order detail: show carrier/status/events; refresh if stale (>15 min) and not delivered
Portal order detail: tracking column
Settings / env
EASYPOST_WEBHOOK_SECRET= in .env.example and .env.prod.example (optional; do not add to required validate-env.sh unless shipping is on and you want it required)
If FEATURE_SHIPPING is false: no tracking UI, no webhook, no dispatcher. Orders still exist under shop.
6. seed_demo — must ship in this template
This repo has noseed_demo today (site/core/management/commands/ is only dispatch_due). Print Forge added a tagged demo seeder (DEMO- / demo+) for client walkthroughs. Port that command (neutral copy, not Print Forge SKUs) and extend it for this ticket so a FEATURE_SHOP=true demo shows accounts/reviews and a FEATURE_SHIPPING=true demo shows tracking.
Print Forge source: site/core/management/commands/seed_demo.py + site/core/tests.pySeedDemoTests. After this ticket, Print Forge seed will also create shoppers/reviews/tracking — copy that extended behavior, not the old catalog-only seed.
--reset deletes only tagged demo rows (including demo shopper users), then reseeds.
No live Stripe, EasyPost, or outbound mail.
Every extra block behind apps.is_installed(...) so clients without shop/shipping still seed contacts/leads.
New rows when FEATURE_SHOP
Non-staff Users for a subset of demo contacts (e.g. demo+jordan@example.com), password documented in command help / stdout (strong enough for AUTH_PASSWORD_VALIDATORS, e.g. Demo-buyer-pass-123).
CustomerProfile with phone + shipping JSON copied from the matching Contact.
Order.user set on paid/fulfilled demo orders; leave at least one guest paid/open order (user=NULL) so claim-on-register can be demoed.
ProductReview rows (1–5) only for users who purchased that product.
--reset must delete User where username matches demo+*@example.com (profiles/reviews cascade).
In transit: tracking_status=in_transit, events, last_tracked_at.
Stub numbers only (no EasyPost API). Notes tagged [demo-seed].
Re-runs update tracking fields on existing demo shipments (not only defaults= on create).
Tests (SeedDemoTests)
Prod refuse; beta allowed; idempotent second run; --reset rebuilds.
If shop installed: demo shopper can login on account:login; order history shows linked DEMO-ORD-*; product detail shows seeded review; guest order still unlinked.
If shipping installed: demo shipment has tracking_status and events.
If shop not installed: command still succeeds; no /account/ routes required.
7. Tests (non-seed)
Port/adapt from Print Forge:
accounts/tests.py — register, profile, claim orders, customer cannot open portal
Password reset — see §3
shop — checkout attaches user + Stripe customer mock; review requires purchase; order history isolation
Manual: shop off → no Sign in / /account/; shipping off → orders without tracking.
10. Acceptance
Shop off: no customer account URLs, no reviews, contact form still slim.
Shop on: register, login, reset password via email, profile shipping, order history, Stripe customer id only, 1–5 review after purchase.
Shipping on: EasyPost or pasted tracking appears on customer order; webhook + dispatch_due update status.
seed_demo (dev/beta) creates shoppers, reviews, tracking when those apps are installed; --reset is safe; prod refused.
Port Print Forge shopper accounts, slim contact form, purchase reviews, Stripe-hosted cards, and shipment tracking into this template. These belong to **catalog features**, not the always-on CRM portal:
| Capability | Feature flag / app |
|---|---|
| Customer register/login/profile, order history, reviews, Stripe customer ids | `FEATURE_SHOP` → `shop` (+ `accounts` customer URLs only when shop is installed) |
| Password reset for shoppers | same (`FEATURE_SHOP`) |
| Tracking status, scan events, EasyPost tracker webhook, paste-in Pirate Ship / carrier numbers | `FEATURE_SHIPPING` → `shipping` |
| Slim public contact form (email + interest + message) | always-on `public`, but keep CRM `Contact` name/phone/address for portal import |
**Do not copy Print Forge branding, copy, SKUs, or demo addresses.** Keep template-neutral labels ("Shop", "Account", generic product names in seed).
Reference implementation: [print_forge#7](https://git.aimloperations.com/ai_ml_operations/print_forge/issues/7) / [print_forge#8](https://git.aimloperations.com/ai_ml_operations/print_forge/pulls/8) (`feature/7-customer-accounts-reviews`). Print Forge also ships `seed_demo`; this template currently has **no** `seed_demo` command — port it and extend it (see §9).
---
## Why
Print Forge added a guest-checkout shop with staff-only `/accounts/login/`. Buyers had no way to see orders, save a shipping address, or review products. Cards must stay on Stripe. Shipping labels already go through EasyPost (and Pirate Ship CSV); customers need tracking feedback without a second Shippo integration.
---
## 1. Slim public contact form (always-on `public`)
**Current template** (`site/public/forms.py`): `first_name`, `last_name`, `email`, `phone`, full address, `interest`, `message`.
**Target**
- Keep: `email` (required), `interest`, `message`, optional reCAPTCHA.
- Remove from the **public form only**: name, phone, address.
- `upsert_contact(..., merge_phone_address=False)` so the form matches on email only.
- Email consent only (no SMS/postcard consent from this form).
- Admin notify email: email + message (drop name/phone/address blocks).
- CRM `Contact` model **unchanged** — portal create/import still collect name/phone/address.
- Update `ContactFormMergeTests`: merge-on-email; extra POST fields ignored.
Print Forge files to port (adapt interest choices back to template: quote/support/other, not toy-specific):
- `site/public/forms.py`
- `site/public/views.py` (`contact`)
- `site/public/templates/public/contact.html`
- `site/public/notifications.py`
- `site/public/templates/emails/contact_email.html` + `.txt`
- `site/contacts/tests_merge.py`
---
## 2. Customer accounts (gate on `FEATURE_SHOP`)
Staff portal login stays at `/accounts/login/` (`RealtorProfile`). Shoppers are **non-staff** Django `User`s.
### Model
`accounts.CustomerProfile` (OneToOne `User`):
- `phone`
- `shipping_address` JSON (same shape as `Contact.make_postal_address`)
- `stripe_customer_id` (blank OK; **never** store PAN/CVC/payment method numbers)
### URLs — only include when `apps.is_installed("shop")`
Mount `/account/` from `accounts.customer_urls` in the project `urls.py` **inside** the existing `if apps.is_installed("shop")` block (do not always-on include it).
| Path | Name | Purpose |
|---|---|---|
| `/account/` | `account:home` | redirect to orders |
| `/account/login/` | `account:login` | email + password, public `base.html` |
| `/account/logout/` | `account:logout` | |
| `/account/register/` | `account:register` | email as username |
| `/account/profile/` | `account:profile` | name, phone, shipping |
| `/account/password-reset/` … | see §3 | |
| `/account/orders/` | `account:orders` | |
| `/account/orders/<uuid>/` | `account:order_detail` | |
### Auth behavior
- Register: `username = email.lower()`, create `CustomerProfile`, `claim_orders_for_user` (attach guest orders with matching email).
- `PortalLoginView`: non-staff success → `account:home`.
- `PortalStaffMiddleware`: authenticated non-staff hitting `/portal/` (except `/webhooks/`) → `account:home`. **Must exempt** `/portal/shop/webhooks/stripe/` and `/portal/shipping/webhooks/easypost/`.
- Public nav: Sign in / Account only when shop enabled; Client portal link stays for staff.
- `UnderConstructionMiddleware`: exempt `/account/login`, `/account/logout`, `/account/register`, `/account/password-reset`.
- `robots.txt`: `Disallow: /account/`.
- Portal tests that `login()` to `/portal/` must create users with `is_staff=True`.
### Stripe (no card storage)
In `shop.services.create_checkout_session`:
- If `order.user`: `ensure_stripe_customer` → Checkout Session `customer=` + `payment_intent_data.setup_future_usage="on_session"`.
- Else: `customer_email=` as today.
- Webhook `checkout.session.completed`: `mark_paid(..., stripe_customer_id=session.customer)` and persist id on `CustomerProfile`.
- Profile copy: "Cards stay on Stripe. We never store card numbers."
### Order model
`shop.Order.user` nullable FK `SET_NULL`, `related_name="shop_orders"`. Checkout attaches the logged-in user and prefills profile shipping/name/email.
Print Forge files:
- `site/accounts/models.py` (`CustomerProfile`)
- `site/accounts/forms.py`, `views.py`, `services.py`, `middleware.py`, `customer_urls.py`
- `site/accounts/templates/accounts/{account_base,customer_login,register,profile}.html`
- `site/shop/models.py` (`Order.user`)
- `site/shop/services.py` (`create_order_from_cart(user=)`, `ensure_stripe_customer`, `remember_stripe_customer`)
- `site/shop/views.py` (checkout prefill, account order views)
- `site/shop/templates/shop/account/`, `checkout.html`, `success.html`
- `site/print_forge/urls.py` → equivalent in template project urls: **shop-gated** include
---
## 3. Shopper password reset (required; not optional)
Print Forge register/login shipped without reset. **This template port must include it** so buyers are not locked out.
Use Django `auth_views.PasswordResetView` / `PasswordResetDoneView` / `PasswordResetConfirmView` / `PasswordResetCompleteView` under `/account/`, public `base.html` templates, email looking up `User.email` (same as username).
- Login page: "Forgot password?" → `account:password_reset`
- Tokens expire per Django defaults
- Tests: POST reset for a shopper → `mail.outbox` has a link; confirm view sets a new password; staff portal login is unchanged
- Dev: console/locmem email; prod: existing SMTP backend + `DEFAULT_FROM_EMAIL`
---
## 4. Purchase-gated reviews (`FEATURE_SHOP`)
`shop.ProductReview`:
- FK product, user, order
- `rating` 1–5 (`MinValueValidator`/`MaxValueValidator` + `CheckConstraint`)
- optional `title`, `body`
- `UniqueConstraint(user, product)` — one review per buyer per product
`qualifying_order_for_review`: paid or fulfilled order containing that product, matched by `user` or email.
Product detail: average + list; form only if logged-in buyer who has not reviewed; POST `shop:review`.
Print Forge: `shop/models.py` `ProductReview`, `shop/services.py` helpers, `shop/views.py` `product_review`, `shop/templates/shop/detail.html`, `shop/public_urls.py`.
---
## 5. Tracking (`FEATURE_SHIPPING` only)
Do **not** add Shippo as a second label buyer. EasyPost remains the shipping API. Pirate Ship stays CSV export. Tracking lookup uses EasyPost **Tracker** API, which works for labels bought in EasyPost **and** tracking numbers pasted from Pirate Ship / the carrier (USPS, UPS, FedEx, DHL).
### `shipping.Shipment` fields
- `tracking_status` (unknown, pre_transit, in_transit, out_for_delivery, delivered, available_for_pickup, return_to_sender, failure, cancelled, error)
- `tracking_url`, `tracker_id`, `tracking_events` JSON, `last_tracked_at`
### Services
- `public_tracking_url(carrier, number)` fallback links
- `apply_tracker_payload` / `refresh_tracking` (POST `https://api.easypost.com/v2/trackers` when `EASYPOST_API_KEY` set; stub `pre_transit` otherwise)
- `attach_tracking` for portal paste-in
- `sync_open_tracking` registered on `register_dispatcher` so `dispatch_due` refreshes labeled, not-yet-delivered rows
- After `buy_label`, call `refresh_tracking`
### Portal + customer UI
- Shipment detail: paste tracking + carrier, refresh, scan table
- CSRF-exempt `POST /portal/shipping/webhooks/easypost/` (`EASYPOST_WEBHOOK_SECRET` optional header/`?token=`)
- Customer order detail: show carrier/status/events; refresh if stale (>15 min) and not delivered
- Portal order detail: tracking column
### Settings / env
- `EASYPOST_WEBHOOK_SECRET=` in `.env.example` and `.env.prod.example` (optional; do not add to required `validate-env.sh` unless shipping is on and you want it required)
Print Forge: `site/shipping/models.py`, `services.py`, `views.py`, `urls.py`, `hooks.py`, `templates/shipping/detail.html`.
If `FEATURE_SHIPPING` is false: no tracking UI, no webhook, no dispatcher. Orders still exist under shop.
---
## 6. `seed_demo` — **must ship in this template**
This repo has **no** `seed_demo` today (`site/core/management/commands/` is only `dispatch_due`). Print Forge added a tagged demo seeder (`DEMO-` / `demo+`) for client walkthroughs. **Port that command** (neutral copy, not Print Forge SKUs) **and** extend it for this ticket so a `FEATURE_SHOP=true` demo shows accounts/reviews and a `FEATURE_SHIPPING=true` demo shows tracking.
Print Forge source: `site/core/management/commands/seed_demo.py` + `site/core/tests.py` `SeedDemoTests`. After this ticket, Print Forge seed will also create shoppers/reviews/tracking — copy that extended behavior, not the old catalog-only seed.
### Rules (keep from Print Forge)
- Refuse `DJANGO_ENV=prod`.
- Allowed on `dev` and `beta`.
- Idempotent keys: `DEMO-` SKUs/order numbers, `demo+…@example.com` users/contacts, `[demo-seed]` notes.
- `--reset` deletes only tagged demo rows (including demo shopper users), then reseeds.
- **No live Stripe, EasyPost, or outbound mail.**
- Every extra block behind `apps.is_installed(...)` so clients without shop/shipping still seed contacts/leads.
### New rows when `FEATURE_SHOP`
- Non-staff `User`s for a subset of demo contacts (e.g. `demo+jordan@example.com`), password documented in command help / stdout (strong enough for `AUTH_PASSWORD_VALIDATORS`, e.g. `Demo-buyer-pass-123`).
- `CustomerProfile` with phone + shipping JSON copied from the matching `Contact`.
- `Order.user` set on paid/fulfilled demo orders; leave at least one **guest** paid/open order (`user=NULL`) so claim-on-register can be demoed.
- `ProductReview` rows (1–5) only for users who purchased that product.
- `--reset` must delete `User` where `username` matches `demo+*@example.com` (profiles/reviews cascade).
### New rows when `FEATURE_SHIPPING`
- At least two labeled demo shipments:
- **Delivered**: tracking number, `tracking_status=delivered`, `tracking_url`, 2+ `tracking_events` (facility → delivered).
- **In transit**: `tracking_status=in_transit`, events, `last_tracked_at`.
- Stub numbers only (no EasyPost API). Notes tagged `[demo-seed]`.
- Re-runs update tracking fields on existing demo shipments (not only `defaults=` on create).
### Tests (`SeedDemoTests`)
- Prod refuse; beta allowed; idempotent second run; `--reset` rebuilds.
- If shop installed: demo shopper can `login` on `account:login`; order history shows linked `DEMO-ORD-*`; product detail shows seeded review; guest order still unlinked.
- If shipping installed: demo shipment has `tracking_status` and events.
- If shop **not** installed: command still succeeds; no `/account/` routes required.
---
## 7. Tests (non-seed)
Port/adapt from Print Forge:
- `accounts/tests.py` — register, profile, claim orders, customer cannot open portal
- Password reset — see §3
- `shop` — checkout attaches user + Stripe customer mock; review requires purchase; order history isolation
- `shipping` — stub buy → `pre_transit`; `attach_tracking`; EasyPost webhook JSON → `in_transit`
- Contact form — §1
- Staff portal tests: `is_staff=True`
---
## 8. Out of scope
- Custom `AUTH_USER_MODEL`
- Storing cards or Stripe PaymentMethod ids locally
- Shippo (or other) as a label provider
- Wallet / Apple Pay UI beyond hosted Checkout
- Changing staff `RealtorProfile` / `/accounts/login/` purpose
---
## 9. Suggested implementation order
1. Slim contact form + tests.
2. `CustomerProfile` + shop-gated `/account/` + staff middleware + portal test `is_staff`.
3. Password reset.
4. `Order.user`, Stripe customer on checkout, order history UI.
5. `ProductReview` + product page.
6. Shipment tracking fields, EasyPost tracker, webhook, customer/portal UI (shipping flag).
7. Port + extend `seed_demo` + tests.
8. Manual: shop off → no Sign in / `/account/`; shipping off → orders without tracking.
## 10. Acceptance
- [ ] Shop off: no customer account URLs, no reviews, contact form still slim.
- [ ] Shop on: register, login, reset password via email, profile shipping, order history, Stripe customer id only, 1–5 review after purchase.
- [ ] Shipping on: EasyPost or pasted tracking appears on customer order; webhook + `dispatch_due` update status.
- [ ] `seed_demo` (dev/beta) creates shoppers, reviews, tracking when those apps are installed; `--reset` is safe; prod refused.
westfarn
self-assigned this 2026-09-07 04:42:36 -07:00
Print Forge source of truth for the extended seed_demo is now on feature/7-customer-accounts-reviews (PR ai_ml_operations/print_forge#8), not the older catalog-only seeder.
Port that command (neutral catalog/copy — do not copy Print Forge SKUs or toy names) plus:
Shoppers (FEATURE_SHOP)
Email
Password
Linked orders
Review
demo+<slug>@example.com (subset of demo contacts)
documented in command help / stdout; Print Forge uses Demo-buyer-pass-123
paid/fulfilled orders get Order.user
1–5 stars on a purchased SKU, body prefixed [demo-seed]
at least one guest contact with an OPEN/CANCELLED order
no User
user=NULL
none — claim-on-register demo
--reset must delete non-staff Users whose username is demo+…@example.comafter demo invoices (do not snapshot created_by before reset; prefer staff created_by, never a demo shopper).
Tracking (FEATURE_SHIPPING)
One delivered labeled shipment with 2+ tracking_events and a stub tracking number.
One in_transit labeled shipment with events + last_tracked_at.
Re-runs update fields on existing rows (not create-only defaults).
Notes tagged [demo-seed]. No EasyPost/Stripe/mail calls.
Password reset
Shopper reset is required on this ticket (§3). Print Forge now has /account/password-reset/ (Django auth views, Forgot password? on login). Copy those templates + tests when porting accounts.
Print Forge source of truth for the extended `seed_demo` is now on `feature/7-customer-accounts-reviews` (PR https://git.aimloperations.com/ai_ml_operations/print_forge/pulls/8), not the older catalog-only seeder.
Port **that** command (neutral catalog/copy — do not copy Print Forge SKUs or toy names) plus:
### Shoppers (`FEATURE_SHOP`)
| Email | Password | Linked orders | Review |
|---|---|---|---|
| `demo+<slug>@example.com` (subset of demo contacts) | documented in command help / stdout; Print Forge uses `Demo-buyer-pass-123` | paid/fulfilled orders get `Order.user` | 1–5 stars on a purchased SKU, body prefixed `[demo-seed]` |
| at least one guest contact with an OPEN/CANCELLED order | no User | `user=NULL` | none — claim-on-register demo |
`--reset` must delete non-staff `User`s whose username is `demo+…@example.com` **after** demo invoices (do not snapshot `created_by` before reset; prefer staff `created_by`, never a demo shopper).
### Tracking (`FEATURE_SHIPPING`)
- One **delivered** labeled shipment with 2+ `tracking_events` and a stub tracking number.
- One **in_transit** labeled shipment with events + `last_tracked_at`.
- Re-runs update fields on existing rows (not create-only defaults).
- Notes tagged `[demo-seed]`. No EasyPost/Stripe/mail calls.
### Password reset
Shopper reset is required on this ticket (§3). Print Forge now has `/account/password-reset/` (Django auth views, `Forgot password?` on login). Copy those templates + tests when porting accounts.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Port Print Forge shopper accounts, slim contact form, purchase reviews, Stripe-hosted cards, and shipment tracking into this template. These belong to catalog features, not the always-on CRM portal:
FEATURE_SHOP→shop(+accountscustomer URLs only when shop is installed)FEATURE_SHOP)FEATURE_SHIPPING→shippingpublic, but keep CRMContactname/phone/address for portal importDo not copy Print Forge branding, copy, SKUs, or demo addresses. Keep template-neutral labels ("Shop", "Account", generic product names in seed).
Reference implementation: print_forge#7 / print_forge#8 (
feature/7-customer-accounts-reviews). Print Forge also shipsseed_demo; this template currently has noseed_democommand — port it and extend it (see §9).Why
Print Forge added a guest-checkout shop with staff-only
/accounts/login/. Buyers had no way to see orders, save a shipping address, or review products. Cards must stay on Stripe. Shipping labels already go through EasyPost (and Pirate Ship CSV); customers need tracking feedback without a second Shippo integration.1. Slim public contact form (always-on
public)Current template (
site/public/forms.py):first_name,last_name,email,phone, full address,interest,message.Target
email(required),interest,message, optional reCAPTCHA.upsert_contact(..., merge_phone_address=False)so the form matches on email only.Contactmodel unchanged — portal create/import still collect name/phone/address.ContactFormMergeTests: merge-on-email; extra POST fields ignored.Print Forge files to port (adapt interest choices back to template: quote/support/other, not toy-specific):
site/public/forms.pysite/public/views.py(contact)site/public/templates/public/contact.htmlsite/public/notifications.pysite/public/templates/emails/contact_email.html+.txtsite/contacts/tests_merge.py2. Customer accounts (gate on
FEATURE_SHOP)Staff portal login stays at
/accounts/login/(RealtorProfile). Shoppers are non-staff DjangoUsers.Model
accounts.CustomerProfile(OneToOneUser):phoneshipping_addressJSON (same shape asContact.make_postal_address)stripe_customer_id(blank OK; never store PAN/CVC/payment method numbers)URLs — only include when
apps.is_installed("shop")Mount
/account/fromaccounts.customer_urlsin the projecturls.pyinside the existingif apps.is_installed("shop")block (do not always-on include it)./account/account:home/account/login/account:loginbase.html/account/logout/account:logout/account/register/account:register/account/profile/account:profile/account/password-reset/…/account/orders/account:orders/account/orders/<uuid>/account:order_detailAuth behavior
username = email.lower(), createCustomerProfile,claim_orders_for_user(attach guest orders with matching email).PortalLoginView: non-staff success →account:home.PortalStaffMiddleware: authenticated non-staff hitting/portal/(except/webhooks/) →account:home. Must exempt/portal/shop/webhooks/stripe/and/portal/shipping/webhooks/easypost/.UnderConstructionMiddleware: exempt/account/login,/account/logout,/account/register,/account/password-reset.robots.txt:Disallow: /account/.login()to/portal/must create users withis_staff=True.Stripe (no card storage)
In
shop.services.create_checkout_session:order.user:ensure_stripe_customer→ Checkout Sessioncustomer=+payment_intent_data.setup_future_usage="on_session".customer_email=as today.checkout.session.completed:mark_paid(..., stripe_customer_id=session.customer)and persist id onCustomerProfile.Order model
shop.Order.usernullable FKSET_NULL,related_name="shop_orders". Checkout attaches the logged-in user and prefills profile shipping/name/email.Print Forge files:
site/accounts/models.py(CustomerProfile)site/accounts/forms.py,views.py,services.py,middleware.py,customer_urls.pysite/accounts/templates/accounts/{account_base,customer_login,register,profile}.htmlsite/shop/models.py(Order.user)site/shop/services.py(create_order_from_cart(user=),ensure_stripe_customer,remember_stripe_customer)site/shop/views.py(checkout prefill, account order views)site/shop/templates/shop/account/,checkout.html,success.htmlsite/print_forge/urls.py→ equivalent in template project urls: shop-gated include3. Shopper password reset (required; not optional)
Print Forge register/login shipped without reset. This template port must include it so buyers are not locked out.
Use Django
auth_views.PasswordResetView/PasswordResetDoneView/PasswordResetConfirmView/PasswordResetCompleteViewunder/account/, publicbase.htmltemplates, email looking upUser.email(same as username).account:password_resetmail.outboxhas a link; confirm view sets a new password; staff portal login is unchangedDEFAULT_FROM_EMAIL4. Purchase-gated reviews (
FEATURE_SHOP)shop.ProductReview:rating1–5 (MinValueValidator/MaxValueValidator+CheckConstraint)title,bodyUniqueConstraint(user, product)— one review per buyer per productqualifying_order_for_review: paid or fulfilled order containing that product, matched byuseror email.Product detail: average + list; form only if logged-in buyer who has not reviewed; POST
shop:review.Print Forge:
shop/models.pyProductReview,shop/services.pyhelpers,shop/views.pyproduct_review,shop/templates/shop/detail.html,shop/public_urls.py.5. Tracking (
FEATURE_SHIPPINGonly)Do not add Shippo as a second label buyer. EasyPost remains the shipping API. Pirate Ship stays CSV export. Tracking lookup uses EasyPost Tracker API, which works for labels bought in EasyPost and tracking numbers pasted from Pirate Ship / the carrier (USPS, UPS, FedEx, DHL).
shipping.Shipmentfieldstracking_status(unknown, pre_transit, in_transit, out_for_delivery, delivered, available_for_pickup, return_to_sender, failure, cancelled, error)tracking_url,tracker_id,tracking_eventsJSON,last_tracked_atServices
public_tracking_url(carrier, number)fallback linksapply_tracker_payload/refresh_tracking(POSThttps://api.easypost.com/v2/trackerswhenEASYPOST_API_KEYset; stubpre_transitotherwise)attach_trackingfor portal paste-insync_open_trackingregistered onregister_dispatchersodispatch_duerefreshes labeled, not-yet-delivered rowsbuy_label, callrefresh_trackingPortal + customer UI
POST /portal/shipping/webhooks/easypost/(EASYPOST_WEBHOOK_SECREToptional header/?token=)Settings / env
EASYPOST_WEBHOOK_SECRET=in.env.exampleand.env.prod.example(optional; do not add to requiredvalidate-env.shunless shipping is on and you want it required)Print Forge:
site/shipping/models.py,services.py,views.py,urls.py,hooks.py,templates/shipping/detail.html.If
FEATURE_SHIPPINGis false: no tracking UI, no webhook, no dispatcher. Orders still exist under shop.6.
seed_demo— must ship in this templateThis repo has no
seed_demotoday (site/core/management/commands/is onlydispatch_due). Print Forge added a tagged demo seeder (DEMO-/demo+) for client walkthroughs. Port that command (neutral copy, not Print Forge SKUs) and extend it for this ticket so aFEATURE_SHOP=truedemo shows accounts/reviews and aFEATURE_SHIPPING=truedemo shows tracking.Print Forge source:
site/core/management/commands/seed_demo.py+site/core/tests.pySeedDemoTests. After this ticket, Print Forge seed will also create shoppers/reviews/tracking — copy that extended behavior, not the old catalog-only seed.Rules (keep from Print Forge)
DJANGO_ENV=prod.devandbeta.DEMO-SKUs/order numbers,demo+…@example.comusers/contacts,[demo-seed]notes.--resetdeletes only tagged demo rows (including demo shopper users), then reseeds.apps.is_installed(...)so clients without shop/shipping still seed contacts/leads.New rows when
FEATURE_SHOPUsers for a subset of demo contacts (e.g.demo+jordan@example.com), password documented in command help / stdout (strong enough forAUTH_PASSWORD_VALIDATORS, e.g.Demo-buyer-pass-123).CustomerProfilewith phone + shipping JSON copied from the matchingContact.Order.userset on paid/fulfilled demo orders; leave at least one guest paid/open order (user=NULL) so claim-on-register can be demoed.ProductReviewrows (1–5) only for users who purchased that product.--resetmust deleteUserwhereusernamematchesdemo+*@example.com(profiles/reviews cascade).New rows when
FEATURE_SHIPPINGtracking_status=delivered,tracking_url, 2+tracking_events(facility → delivered).tracking_status=in_transit, events,last_tracked_at.[demo-seed].defaults=on create).Tests (
SeedDemoTests)--resetrebuilds.loginonaccount:login; order history shows linkedDEMO-ORD-*; product detail shows seeded review; guest order still unlinked.tracking_statusand events./account/routes required.7. Tests (non-seed)
Port/adapt from Print Forge:
accounts/tests.py— register, profile, claim orders, customer cannot open portalshop— checkout attaches user + Stripe customer mock; review requires purchase; order history isolationshipping— stub buy →pre_transit;attach_tracking; EasyPost webhook JSON →in_transitis_staff=True8. Out of scope
AUTH_USER_MODELRealtorProfile//accounts/login/purpose9. Suggested implementation order
CustomerProfile+ shop-gated/account/+ staff middleware + portal testis_staff.Order.user, Stripe customer on checkout, order history UI.ProductReview+ product page.seed_demo+ tests./account/; shipping off → orders without tracking.10. Acceptance
dispatch_dueupdate status.seed_demo(dev/beta) creates shoppers, reviews, tracking when those apps are installed;--resetis safe; prod refused.Print Forge source of truth for the extended
seed_demois now onfeature/7-customer-accounts-reviews(PR ai_ml_operations/print_forge#8), not the older catalog-only seeder.Port that command (neutral catalog/copy — do not copy Print Forge SKUs or toy names) plus:
Shoppers (
FEATURE_SHOP)demo+<slug>@example.com(subset of demo contacts)Demo-buyer-pass-123Order.user[demo-seed]user=NULL--resetmust delete non-staffUsers whose username isdemo+…@example.comafter demo invoices (do not snapshotcreated_bybefore reset; prefer staffcreated_by, never a demo shopper).Tracking (
FEATURE_SHIPPING)tracking_eventsand a stub tracking number.last_tracked_at.[demo-seed]. No EasyPost/Stripe/mail calls.Password reset
Shopper reset is required on this ticket (§3). Print Forge now has
/account/password-reset/(Django auth views,Forgot password?on login). Copy those templates + tests when porting accounts.