Thames Valley Gifts Storefront — Expected Output Spec
DRAFT — not approved. Written by an agent from the research folder and the founder decisions listed in §1. The founder confirms or corrects it in a Stage-2 review. Until then, CLAUDE.md Rule #17 blocks storefront code (WP2).
Markers used in this file:
- FOUNDER DECISION: something only the founder can decide. A recommended default is given where one exists.
- DECIDED 2026-09-28 (default adopted by orchestrator): a FOUNDER DECISION whose recommended default was accepted on the founder's behalf so WP2 planning can proceed; the founder can still override it. See the Decision log at the end of this file.
- [UNVERIFIED]: a fact nobody has checked yet. Check it before building on it.
- [PLACEHOLDER]: a value that must be replaced before launch. Wherever it appears, it must be labelled as such.
0. Scope
This spec covers everything a shopper sees on https://thamesvalleygifts.ca up to and including the screen that says a proof request was received:
- Home
- Shop and collections (by occasion, maker and product type)
- The makers index and maker pages
- The product detail page (PDP)
- The coming-soon waitlist
- The bulk / B2B quote page
- Guides, policy pages and contact
- The request-submitted confirmation
- The server contract for submitting a request (
POST /api/tvg/order-requests)
Sibling specs. Their content is assumed here, not repeated:
| Spec | Covers |
|---|---|
tvg-proof-to-pay.md (written with this one) | Everything after "Request a proof": the confirmation email, proof, approval, payment, fulfilment, refunds, every email, and the state machine. It is the canonical source for the confirmation screen's wording, and §4.11 here mirrors it. If the two differ, tvg-proof-to-pay.md wins. |
tvg-ops-and-payouts.md (not written yet) | The founder's ops pages, catalogue editing, makers, payouts. |
tvg-ai-front-desk.md (not written yet) | The chat widget and the voice line. No chat widget may appear on the storefront until that spec is approved. |
Out of scope at launch (architecture §2.10): cart, customer accounts or logins, site search, reviews, discount codes, gift cards, SMS, international shipping, maker portal.
Screenshots: capture into knowledge/acceptance/screenshots/tvg-storefront/ from the deployed host once WP2 is live (names are given per touchpoint). None exist yet.
1. Fixed facts and founder decisions already made
These are settled. Agents do not reopen them.
| # | Fact / decision | Source |
|---|---|---|
| F1 | Platform: Option D. TVG is built inside churchwiseai-web, served at /tvg/*, and rewritten from thamesvalleygifts.ca. There is no cart at launch. | Founder, 2026-09-28 |
| F2 | Payments stay on the existing Stripe account. | Founder |
| F3 | Design: "Main Street". Drinkware, totes and the general shop use a bright small-town shop register. Memorial, baptism, ordination and wedding pages use a quieter keepsake register. | Founder |
| F4 | Every page footer carries exactly: "Thames Valley Gifts is a division of ChurchWiseAI LTD." | Business Names Act; BRAND.md |
| F5 | Ontario business-name registration BIN 1001760694. This is not the GST/HST number and must never be labelled as one (see §3.6). | BRAND.md |
| F6 | Personalized items are final sale unless defective or different from the approved proof. This is disclosed before the request is submitted, together with an "I have checked the spelling" confirmation. | Founder |
| F7 | The delivery window runs from proof approval. | Founder |
| F8 | Pickup is by appointment in Ingersoll and is free. | Founder |
| F9 | Shipping is Canada only at a flat rate: $14.50 [PLACEHOLDER] until the founder sets it. | Founder |
| F10 | Scripture products offer KJV or WEB. NET comes later and is not offered at launch. | Founder |
| F11 | Product statuses are example, coming_soon and available, with a server-side gate: an example product accepts no request; a coming_soon product accepts a waitlist signup only; a product can be available only when its maker has a signed agreement. | Founder |
| F12 | The founder's own products (the ChurchWise Gifts label) are not available before mid-November 2026. | Founder |
| F13 | No invented reviews and no "bestseller" claims. Example listings are visibly labelled as examples. | Founder; BRAND.md positioning |
2. User states and product statuses
2.1 User states
| State | Who | How they arrive | What is different for them |
|---|---|---|---|
| U1 Anonymous shopper, phone (375 px baseline) | A gift buyer on a phone, often from Facebook or a Google result | Home, a collection or a PDP link | Single column. The live preview stays in view while they type (§7). |
| U2 Anonymous shopper, desktop (≥1024 px) | Same buyer on a laptop | Same | Two columns on the PDP: gallery and preview on the left, form on the right. |
| U3 B2B buyer | A church office, sports team, realtor or business ordering 10 or more pieces, or different names per piece | Home "Bulk orders", the bulk link on a PDP, or a direct link to /bulk | Uses /bulk. Sees quantity tiers on PDPs. Is told organizations can pay by card or Interac e-Transfer after approving the proof. |
| U4 Returning customer with a request in flight | Someone who has already submitted a request | The link in their email, or back to the site | The storefront has no accounts and no memory of them. They reach their request only through the emailed link or "Find my request" (§4.10). The storefront shows them nothing personal. |
There is no logged-in state on the storefront. The ops pages are on churchwiseai.com/founder/[token]/tvg/* and are out of scope here.
2.2 Product statuses: what each one shows, everywhere
| Status | Listed in /shop, home, maker page? | Tile shows | PDP shows | Actions allowed | Server gate on POST /api/tvg/order-requests |
|---|---|---|---|---|---|
available | Yes | The "Available" label, the price ("$29.00" or "from $29.00"), and the maker name | The full personalization form, the live preview, the price, the final-sale disclosure and "Request a proof" | Request a proof | kind='retail' or 'bulk_quote' accepted |
coming_soon | Yes | The "Coming soon" label, the status_note (for example "Orders open mid-November 2026") and the maker name. No price. | Gallery, description, a preview-only personalization panel (§4.6), and the waitlist form. No "Request a proof". | Join the waitlist | Only kind='waitlist' accepted. retail and bulk_quote get 409 |
example | Yes, always carrying the label | The "Example" label, the text "Example listing — not for sale" and the maker name. No price. | Gallery (each image labelled), description, and a plain list of the personalization options. No form inputs, no preview interaction, no request button, no waitlist. | Browse only | Every kind gets 409 |
paused | No | — | If reached by direct URL: title, gallery, and "This item isn't taking requests right now." No form. | None | 409 |
draft, retired | No | — | 404 | None | 409 (or 404 if the id is unknown) |
The three status labels are a fixed set. Each one is text, never colour alone:
- "Available": solid fill.
- "Coming soon": outline.
- "Example": striped or dashed, like a sample tag.
Price rule. A price appears only on available items. Example items never show a price. Nothing J & B Creations prices appears anywhere (jb_creations_reference rules).
3. Global rules (every page)
3.1 Header
Should see:
- The "Thames Valley Gifts" wordmark, linking to
/. - Nav: Shop · Occasions · Makers · Bulk orders · Help (Help goes to
/contact). - On phones, the nav collapses into a menu button labelled "Menu", which is a real
<button>witharia-expanded. - A thin announcement bar: "You approve a proof before we make it · Free pickup in Ingersoll · Ships across Canada".
Should NOT see:
- "Cart", a basket icon, or a "(0)" counter. The design mock shows "Cart (0)"; that is out of scope at launch.
- "Sign in", "Account" or "My orders".
- A site search box (deferred).
- Any ChurchWiseAI AI-product promotion, PewSearch links, or the words "AI-powered" in marketing copy.
3.2 Footer
Should see:
- Links to Shipping, Returns & final sale, Privacy, Terms, About, Contact, and "Find my request".
- The exact line "Thames Valley Gifts is a division of ChurchWiseAI LTD." on every page, including the confirmation screen and 404.
- "Prices in Canadian dollars."
- "Pickup in Ingersoll by appointment."
Should NOT see:
- A newsletter signup.
- Social "follow us" counts.
- Payment badges for methods TVG does not accept. Card, Apple Pay and Google Pay may be shown. No "Interac" badge, because Interac is not available at online checkout. E-transfer is a B2B-only manual option.
- A GST/HST number labelled with the BIN.
3.3 Registers (F3)
- Every page root exposes
data-register="main_street"ordata-register="keepsake"so QA can assert it. - Keepsake register applies to:
- the collection pages
/shop?occasion=memorial|baptism|ordination|wedding; - any PDP whose product register is
keepsake; /guides/verses.
- the collection pages
- DECIDED 2026-09-28 (default adopted by orchestrator): add an explicit
registerfield totvg_products(not in architecture §2.2). The default comes from the product's first occasion: memorial, baptism, ordination or wedding giveskeepsake, anything else givesmain_street, and the operator can override it. Every occasion not in that list is Main Street.
| Main Street | Keepsake | |
|---|---|---|
| Palette | Bright tiles (Oxford green, sweet corn, Tillsonburg sky per design doc) | Quiet ground, one accent colour. No sweet-corn yellow fills, no coloured product tiles. |
| Copy tone | Friendly and direct ("Put their name on it.") | Calm and plain. No exclamation marks, no puns, no emoji in page copy. |
| Cross-sell | "More from this maker" allowed | Only items from the same occasion, or none. Never drinkware or novelty items next to a memorial piece. |
| Bulk tier promo banner | Allowed | Not shown, but the tier table stays if the product has tiers. |
Contrast. Sweet corn (#F2C230) fails 4.5:1 as text on white. It may be a fill behind dark text, but never text or a thin outline on white.
3.4 Honesty rules (F13, BRAND.md)
Should NOT appear anywhere on the storefront:
- Star ratings, review counts, testimonials, "Top rated", "Bestseller", "Popular", "Trending", "Customer favourite", or "X sold".
- "N people are viewing this", countdown timers, "Only N left", or "Sale" / strike-through prices without a real prior price.
- "As seen on" logos, media mentions, or "years in business" claims.
- Stock photos, or any other maker's photos, presented as TVG work. No J & B Creations photos are copied.
- Any claim not true on launch day ("hand-inspected by every maker", "made within 100 km") unless the founder confirms it.
Should appear:
- Every image that is not a photo of a real finished piece is labelled on the image: "Illustration", or "Sample image" for example listings.
- Every value marked [PLACEHOLDER] in this spec, while it remains unconfirmed, keeps the site in "Opening soon" mode (§3.7). Placeholder values are never presented as final to a paying customer.
- Allowed true claims:
- named towns (Woodstock, Ingersoll, Tillsonburg, Dorchester, London);
- "You approve a proof before we make it";
- "Made nearby";
- "A person looks at every photo before it is engraved" (true by process: the operator reviews uploads before proofing).
3.5 Fixed copy strings
These strings are exact. Any change is a spec change first.
| Id | Where | Exact text |
|---|---|---|
| C1 | Every footer | Thames Valley Gifts is a division of ChurchWiseAI LTD. |
| C2 | Next to every shown price | HST added at checkout |
| C3 | Example tile and example PDP | Example listing — not for sale |
| C4 | PDP final-sale disclosure (above the checkbox) | Personalized items are final sale. If your item arrives defective or different from the proof you approved, we will remake it or refund you. |
| C5 | PDP spelling checkbox label | I have checked the spelling of every name, date and word, and I understand personalized items are final sale unless defective or different from the approved proof. |
| C6 | PDP primary button | Request a proof |
| C7 | Under the button | Nothing is charged now. We email you a proof to approve first. |
| C8 | Preview disclaimer | Preview is approximate. Your proof shows exactly what will be made. |
| C9 | Photo fallback (no live preview) | We can't show a live preview of photo engraving. A person checks your photo and your proof shows exactly how it will look. |
| C10 | Low-resolution acknowledgement checkbox | I understand this photo may lose detail when it is engraved. |
| C11 | Pickup line | Pickup: free in Ingersoll, by appointment |
| C12 | Shipping line | Shipping: $14.50 flat anywhere in Canada [PLACEHOLDER value] |
| C13 | Canada-only note | We ship within Canada only. |
| C14 | Timing line template | Proof by email within 1 business day. Ready {min}–{max} business days after you approve your proof. |
| C15 | Same-personalization note (quantity > 1) | Every item in this request gets the same personalization. Need a different name on each? Request a bulk order. |
| C16 | Translation note (verse fields) | The World English Bible (WEB) uses the name "Yahweh" where the King James Version (KJV) says "the LORD". Example, Psalm 23:1 — KJV: "The LORD is my shepherd; I shall not want." WEB: "Yahweh is my shepherd; I shall lack nothing." |
| C17 | Coming-soon PDP note | Preview only — orders open {status_note}. What you type here isn't saved. |
| C18 | Paused PDP | This item isn't taking requests right now. |
FOUNDER DECISION: C14's "within 1 business day" proof promise, given 1–2 operator hours a day. Recommended: keep it. If it can't be met, change the string, never miss it silently.
3.6 Money and tax wording
- Prices are CAD, tax-exclusive, and formatted
$84.00. The currency appears once on each PDP ("$84.00 CAD"). - C2 ("HST added at checkout") sits next to every price shown. In this shop "checkout" means the proof and pay step. The proof page shows the exact tax line before any payment (
tvg-proof-to-pay.md). - The PDP never shows a computed tax amount. Tax depends on the destination province (architecture §2.5, accountant sign-off pending), and the design mock's "HST (13%) $10.92" line is not carried into the build.
- [UNVERIFIED] The label "HST" is wrong for provinces that charge GST only. C2 stays as fixed by the founder. The proof shows the correct jurisdiction label.
- The GST/HST registration number that goes on receipts is ChurchWiseAI LTD's CRA business number plus RT0001. BRAND.md confirms the RT0001 account exists; the 9-digit BN is not recorded there. DECIDED 2026-09-28 (default adopted by orchestrator): the number is supplied at runtime via an environment variable (never committed to git); the 9-digit BN itself remains [UNVERIFIED] until the founder supplies it. [UNVERIFIED] The BIN 1001760694 (F5) is the Ontario business-name number. It may appear on the About/Terms page as "Ontario business name registration 1001760694" but must never be labelled GST/HST.
- DECIDED 2026-09-28 (default adopted by orchestrator): show the BIN publicly, on the About page only.
3.7 Pre-launch "Opening soon" mode
Conditions. The site stays in this mode until all of these are true, and the founder switches it off:
- the shipping rate is confirmed (F9);
- the accountant has ruled on the tax table (architecture §2.5);
- at least one product is
availablewith a signed maker agreement.
While in this mode:
- A banner on every page reads "Opening soon — you can look around, but we aren't taking orders yet."
- Every page is
noindex. POST /api/tvg/order-requestsacceptskind='waitlist'only. Everything else gets 409.
3.8 Chat widget
No chat widget, "Ask us" bubble or AI assistant appears until tvg-ai-front-desk.md is approved.
4. Touchpoints
4.1 Home /
States: U1–U4. Screenshots: home-desktop.png, home-375.png.
Should see, top to bottom:
- The announcement bar (§3.1), and the "Opening soon" banner while §3.7 applies.
- A hero with a plain headline and a sub-line naming the real area: "Personalized gifts from makers in Oxford County, Middlesex and London." It has one primary link, "Shop gifts" (to
/shop), and one secondary link, "Bulk orders for churches, teams and businesses" (to/bulk). - A product-family strip linking to filtered
/shop: Tumblers & mugs · Slate & wood · Totes & towels · Faith gifts · Realtor closing gifts · Church & team orders. Only families with at least one listed product appear. - The featured grid:
availableproducts first, thencoming_soon. Every tile shows its status label, maker name, and a price only ifavailable. - "How it works", in exactly three steps:
- "Choose and personalize"
- "We email you a proof — nothing is charged"
- "Approve, pay, and we make it"
- "Made nearby", a makers strip. Each maker shows photo or initials, name, town and specialty, linking to the maker page. Example makers carry the "Example" label.
- A trust strip with true statements only (§3.4): proof before production · free pickup in Ingersoll · $14.50 flat shipping in Canada [PLACEHOLDER] · prices in CAD.
- The footer (§3.2).
Should NOT see:
- Reviews or ratings, "bestseller", a cart, sign-in, or a chat bubble (§3.8).
- Example products in the featured grid. They appear only via the makers strip and maker pages.
- A price on any tile that isn't
available.
Conditional:
- IF zero products are
available(likely before mid-November, F12): the featured grid is headed "Coming soon" and showscoming_soonitems with theirstatus_note. It never shows an empty grid and never implies anything can be ordered.
Success criteria: within 8 seconds a shopper can tell what is sold, that it's made nearby, that they approve a proof before paying, and where to start.
4.2 Shop and collections /shop
Query parameters: ?type=, ?occasion=, ?maker=, ?status=. Screenshots: shop-all.png, shop-occasion-memorial.png, shop-maker-jb.png.
Should see:
- A page heading that reflects the filter, for example "Memorial gifts" or "From J & B Creations".
- Filter chips:
- Occasion: memorial · baptism · ordination · wedding · anniversary · new home / realtor closing · birthday · Christmas · church & team.
- Maker.
- Product type.
- Status: Available / Coming soon / Examples.
- Active filters show as removable chips with a text "×" and the label "Remove filter: Memorial".
- A grid of tiles. Each tile has:
- the image (with "Illustration" or "Sample image" label where §3.4 applies);
- the title;
- "by {Maker}" (maker name always visible, as on NOTHS);
- the status label;
- the price only if
available(the "from" price when options change it); status_noteifcoming_soon, or C3 ifexample.
- Sort order:
available, thencoming_soon, thenexample, thensort_order. - The keepsake register (§3.3) on memorial, baptism, ordination and wedding occasion pages.
Should NOT see:
- Out-of-stock language (items are made to order).
- Draft, paused or retired products.
- Items outside the filter.
- Infinite scroll without a way to reach the footer. Use pagination or "Show more" at 24 per page.
Conditional:
- IF a filter returns 0 items: show "Nothing here yet." plus a link to the unfiltered shop. Never show a blank grid.
- IF
?status=availablereturns 0: show "Nothing is open for orders yet. Here's what's coming." plus thecoming_soonitems.
Links: a tile goes to /makers/{maker}/{product}. The maker name on a tile goes to /makers/{maker}.
4.3 Makers index /makers ("Made nearby")
Should see:
- The heading "Made nearby".
- One sentence on the model, with no claim about principal or agent. For example: "Thames Valley Gifts sells personalized gifts made by local makers. Each piece is made by the maker named on it."
- One card per maker whose status is
onboardingoractiveand who has at least one listed product. The card shows photo or initials, display name, town, specialties, and "Example" if the maker has no signed agreement.
Should NOT see:
- Maker home addresses, personal phone numbers or personal emails.
- A commission rate.
- "Sell with us" forms. DECIDED 2026-09-28 (default adopted by orchestrator): no maker-recruitment link at launch. Use the contact page.
4.4 Maker page /makers/[maker]
Screenshots: maker-churchwise-gifts.png, maker-jb-example.png.
Should see for every maker:
- Name, town, specialties,
story_md, years making (only if the maker supplied it), and a photo (only a real photo supplied by the maker). - A grid of their listed products with status labels.
- The line: "Every order from {Maker} goes through Thames Valley Gifts: you approve a proof, then pay, then {Maker} makes it."
Maker-specific:
ChurchWise Gifts (the founder's own label):
- Town is Ingersoll.
- Products are
coming_soon, withstatus_note"Orders open mid-November 2026", until the founder flips them (F12). - Its agreement gate is satisfied by an internal record. DECIDED 2026-09-28 (default adopted by orchestrator): a self-agreement row is acceptable,
agreement_version='internal'. - The story is written or approved by the founder. No "pastor" credential claim appears unless the founder writes it.
J & B Creations:
- The maker card and page carry the "Example" label and the sentence "Sample maker profile. Details to be confirmed with J & B Creations before anything is for sale."
- Their products are
exampleonly. - Should NOT see:
- J & B photos, prices (including the $20 / $16 chalkboard prices), reviews or the Alignable recommendation quote;
- the Forest Grove street address, either phone number, or the Gmail address;
- claims about their turnaround.
- Town "Dorchester" appears only after Jackie Rayner confirms it.
- [UNVERIFIED] and FOUNDER DECISION (blocking): has Jackie Rayner agreed, in writing, to her business name appearing on a public example profile? If not, the J & B page stays unpublished (
status='onboarding'and hidden) and no J & B example product is listed. Interim rule until consent is obtained: any placeholder maker card shown in mockups or examples uses the neutral name "Example maker: a Dorchester sublimation studio" instead of J & B Creations or Jackie Rayner's name.
Links: product tiles go to the PDP. Contact goes to /contact. There is no direct maker contact.
4.5 Product detail page /makers/[maker]/[product] (status available)
States: U1–U4. Screenshots: pdp-text-desktop.png, pdp-text-375.png, pdp-verse.png, pdp-monogram.png, pdp-photo.png, pdp-final-sale.png.
The PDP is a single form that submits one request. Sections appear in this order. On desktop, (a) is on the left and (b)–(p) on the right. On phones they stack, with the preview pinned (§7).
(a) Gallery and live preview
Should see:
- Hero image, detail images, a scale image (a hand, cup or ruler) where one exists, and colour swatches.
- Each image has meaningful
alttext. - The live preview. When the product's
preview.modeistext_overlay, the first gallery slot is the preview: the customer's text drawn as SVG overpreview.base_image, in the production font files (src/fonts/tvg.ts, OFL-licensed). - Preview update behaviour:
- It updates on every input change: text, verse, translation, monogram, font or colour.
- It updates within 150 ms of the last keystroke. No animation when
prefers-reduced-motion: reduce; the preview swaps instantly. - Its
alttext restates the content, for example: "Preview: slate coaster engraved 'The Andersons · Est. 2019' in Classic serif". - A polite live region announces "Preview updated" at most once per pause in typing (debounced to 1 second or more), not on every keystroke.
- Text that doesn't fit the box shrinks (
fit: shrink) down to a minimum size. Below that minimum, an inline note says "This is getting small. Shorter text will engrave more clearly."
- Empty fields show the field's label in grey placeholder style inside the preview, for example "Name here", so the layout is visible before typing.
- C8 appears directly under the preview.
Photo-engraving fallback (preview.mode: "none" with before/after examples):
- There is no live render. C9 appears where the preview would be.
- Before/after pairs appear only if they are real TVG work, labelled "Example of a photo engraved on slate. Not your photo." If there are no real pairs yet, show nothing rather than stock images.
- The uploaded photo's thumbnail may be shown, labelled "Your photo (not a preview of the engraving)".
Should NOT see:
- A preview claiming to be exact.
- A preview for photo engraving.
- Autoplay carousels.
- Zoom that traps keyboard focus.
(b) Title, maker line and status
- Title (H1). The maker line: "Made nearby by {Maker}, {Town}", linking to the maker page. The "Available" label.
(c) Price
Should see:
- The base price as
$84.00 CAD, with C2 ("HST added at checkout") right next to it. - When options or quantity change the price, a live line reads "Estimated subtotal: $X before HST and shipping. Your proof shows the final total." It is recalculated in the browser with the same
pricing.tsmodule the server uses.
Should NOT see: a tax amount, a "total", a strike-through price, or "Free shipping".
(d) Personalization panel: general rules for every field
Should see:
- Overall instructions before the form fields, per W3C: "Fields marked Required must be filled in. Spaces and punctuation count toward character limits."
- Every field has a visible
<label>. The limit and required/optional status are inside the label text, for example "Name (required, up to 28 characters)". Placeholder text is never the only label. - Errors appear inline next to the field, as an icon plus text, never colour alone. The error names the field, says how to fix it, and is linked with
aria-describedby. - Field order follows
personalization_schema.fields.
Should NOT see: tooltip-only instructions, or a validation summary as the only error display.
(e) Text field (type: text), for example line1 with a 24-character limit
Should see:
- The label, for example "Engraving line 1 (required, up to 24 characters)".
- A counter below the field, "12 / 24", updated on every input.
- At 80% of the limit or more, the counter changes colour (Tillsonburg sky) and adds the text "(4 left)".
- At the limit it reads "24 / 24 (limit reached)".
maxlengthequal to the limit, so the 25th character cannot be typed.- Paste handling: when pasted text would exceed the limit, the field keeps the first N characters and an inline message appears: "Only the first 24 characters fit. Check the end of your text." This design answers the Etsy-truncation failure: truncation is never silent.
- Character set: Latin letters including accents (é, è, ç, ñ, ö and similar), digits, spaces, and
& . , ' - · ! ? ( ) / #. DECIDED 2026-09-28 (default adopted by orchestrator): this set is confirmed as the launch character set, pending a final check against the production font files' actual glyph coverage.- An unsupported character (emoji, CJK, and so on) is refused with an inline message naming it: "This character can't be engraved: ☺. Please remove it."
- Submit is blocked until it is removed. The server enforces the same rule.
- Leading and trailing spaces are trimmed on submit. Double spaces inside the text are kept and shown as-is in the read-back (§4.5 (m)).
Should NOT see: a counter that disagrees with the server's count. Both count Unicode code points after NFC normalization.
DECIDED 2026-09-28 (default adopted by orchestrator): NFC-normalize first, then count code points (not grapheme clusters), for accented names typed with combining marks.
(f) Textarea (type: textarea)
The same rules as (e), with a multi-line counter. For engraved text, line breaks count as 1 character each. The gift note is covered in (l).
(g) Verse picker (type: verse)
Should see:
- The label "Scripture verse", plus "(required)" or "(optional)".
- Translation choice: a radio group, KJV ("King James Version") | WEB ("World English Bible"). The default is the schema's
default_translation(KJV). - C16, the WEB "Yahweh" note, placed next to the translation choice and visible without expanding anything.
- Verse list:
- A select or list of curated references from
tvg_verses. It only offers references that have human-verified text in both translations, where the field offers both. - Each option shows the reference and a fit hint from word count against
max_words: "fits well", "longer, smaller letters", or "too long for this item". Options marked "too long for this item" are disabled, and the reason is shown in text.
- A select or list of curated references from
- The full verse text exactly as it will be engraved appears below the selection, with reference and translation ("Psalm 23:1 · KJV").
- Switching translation updates the shown text and the preview immediately.
- A link to
/guides/verses, and "Want a verse that isn't listed? Ask us" (to/contact). Custom references are not allowed at launch (allow_custom_reference: false).
Should NOT see:
- NET or any other translation (F10).
- A free-text verse box.
- Verse text fetched live from an API.
- A verse with only one verified translation offered in both.
(h) Monogram (type: monogram, 3 letters)
Should see:
- The label "Monogram (required, 3 letters)".
- Three separately labelled one-letter inputs: "First name initial", "Middle name initial" and "Last name initial".
- Each accepts A–Z only, auto-uppercases, and moves focus forward after one letter.
- Each is still editable with Backspace and Shift+Tab.
- Arrangement choice (radio group with a visual example for each):
- "Traditional — first, LAST, middle (last initial larger in the centre)". This is the default
first_LAST_middle. - "In name order".
- "Couple — first person's initial, shared LAST initial, second person's initial". DECIDED 2026-09-28 (default adopted by orchestrator): this gender-neutral wording replaces "hers … his", keeping the same convention.
- "Traditional — first, LAST, middle (last initial larger in the centre)". This is the default
- A plain sentence that reads the result back: "Your monogram will read M J A, with J larger in the centre." It updates live.
- A link to
/guides/monogram. - The preview renders the arrangement. For first=M, middle=A, last=J with Traditional, the preview reads "MJA", with J larger and centred.
Should NOT see: a single free-text "monogram" box. That is the letter-order failure named in ux_research §2 and jb_creations §3.4.
(i) Photo upload (type: image_upload)
Should see:
- The label "Your photo (required, JPEG or PNG, up to 15 MB)". The formats and size come from the schema.
- Resolution guidance computed from the product's print area. For a 6 × 4 in area: "Best: at least 1800 × 1200 pixels. Works: at least 900 × 600 pixels. Clear subject, even light, good contrast."
- A choose-file control that works from phone camera rolls.
accept="image/jpeg,image/png,image/webp". [UNVERIFIED] iOS converts HEIC to JPEG with thisacceptvalue. Test it on a real iPhone before the copy mentions iPhone photos. - After a file is chosen, the browser reads the pixel size and shows the effective DPI rating as text and icon:
- Good (≥300 DPI): "Good — this photo is sharp enough."
- OK (≥150): "OK — this should engrave well."
- Low (<150): "Low resolution — this photo may lose detail when engraved." The unticked checkbox C10 appears and is required to submit.
- A low-resolution photo is never silently rejected. The only hard rejections are the wrong file type or a file over the size limit. Each shows a message naming the problem: "This file is 22 MB. The limit is 15 MB."
- The option "I'll send my photo later", a checkbox. It creates the request with
status='needs_info', and the confirmation screen says the photo is still needed. - The sentence: "A person looks at every photo before it is engraved and will write to you if it won't engrave well."
- A privacy line: "We remove location data from your photo and delete it 90 days after your order is complete." It links to
/privacy.
Should NOT see:
- A live engraving preview.
- Crop or filter tools claiming to show the final result.
- Upload to a public bucket (architecture §2.6: private bucket, signed URLs).
(j) Choice fields: colour, size, font (type: choice)
Should see:
- A radio group per field, with the label containing the field name and "(required)" where needed.
- Colour swatches always carry a text label ("White"). The selected state is shown with a border and a check icon.
- Font swatches render a sample word in that font, plus its text label ("Classic serif").
- Size options show their dimensions and any price change ("8 × 10 in (+$10.00)").
- Choosing an option updates the preview (font, colour) and the estimated subtotal.
Should NOT see: colour-only swatches, or font names shown without a sample.
(k) Quantity and tiers
Should see:
- A number stepper labelled "Quantity", with
minandmaxfrom the schema,inputmode="numeric", and − and + buttons of at least 44 × 44 px. - A tier table when tiers exist, for example "1–9: $28.00 each · 10–24: $22.00 each · 25+: $19.00 each". The row that applies to the current quantity is highlighted in text ("Your price") as well as colour.
- C15 whenever quantity is more than 1.
- When the requested quantity is above
max: the stepper stops atmax, and the inline message "For more than {max}, request a bulk quote" links to/bulk?product={slug}&qty={n}.
(l) Gift note
Should see:
- The label "Gift note (optional, not engraved, up to 250 characters)", with a counter.
- Visual separation from the engraved fields (its own group heading, "Gift note").
Should NOT see: gift-note text in the preview.
(m) Read-back, final sale and spelling confirmation
Should see, in this order, directly above the button:
- "Check your personalization", a read-back box listing every produced value exactly as it will be sent:
- each text field in a monospaced style, with visible markers for double spaces;
- the verse reference, translation and full text;
- the monogram letters and arrangement sentence;
- font, colour and size;
- quantity;
- photo filename and rating, or "Photo: sending later". Each row has an "Edit" link that focuses the field.
- C4, the final-sale disclosure, as body text (not small print).
- C5, the spelling and final-sale checkbox: unticked by default, a real checkbox with a label.
- The contact and fulfilment fields (n).
- C6, "Request a proof", with C7 under it.
Behaviour:
- The button is always enabled. Clicking it with problems moves focus to the first problem and shows its inline message.
- If C5 is unticked, the checkbox gets focus and the message "Please confirm you've checked the spelling."
- A disabled button with no reason given is never used.
Should NOT see:
- The final-sale terms only on a policy page.
- A pre-ticked checkbox.
- The final-sale sentence merged into Terms acceptance.
(n) Contact and fulfilment (part of the same form)
Should see:
- "Your name (required)".
- "Email (required — your proof is sent here)",
type="email". - "Phone (optional)",
type="tel". - Fulfilment, a radio group:
- C11 ("Pickup: free in Ingersoll, by appointment").
- "Ship to me: $14.50 flat anywhere in Canada [PLACEHOLDER]".
- If Ship is chosen, address fields appear: line 1, line 2 (optional), city, a province select listing only the 13 provinces and territories, and postal code validated
A1A 1A1(case-insensitive, space optional). Country is fixed as "Canada" and shown as text, not a field. C13 appears. - "Needed by (optional)", a date input. If the date is earlier than today plus 1 business day plus
lead_time_max_bdplus shipping transit, an inline note reads: "That's sooner than we can usually make it. We'll tell you honestly when we send your proof." Submission is not blocked. - A hidden honeypot field.
Should NOT see:
- A country selector.
- A pre-ticked or required marketing-email checkbox. DECIDED 2026-09-28 (default adopted by orchestrator): no optional news opt-in box at launch, because no promotional program exists.
- A password or account field.
(o) Shipping, pickup and timing lines
Should see (near the price on desktop, and after the button on mobile):
- C11.
- C12.
- C14, filled from
lead_time_min_bd/lead_time_max_bd. - "Shipping adds travel time after that."
[UNVERIFIED] The carrier is Canada Post tracked. Do not name a carrier until the founder confirms.
(p) Below the form
- A description (
description_md) and specifications: materials, size, care ("Hand wash" etc. only if the maker supplied it). - Links to the guides and to "Returns & final sale".
- "More from {Maker}" in the Main Street register only (§3.3).
Should NOT see on the PDP at all:
- "Add to cart", "Buy now", "Pay", or wallet buttons (Apple Pay / Google Pay).
- A second primary button. "Request a proof" is the only primary action.
- Any price or tax total presented as final.
Success criteria: a shopper can personalize, see their text on the product, get a clear read-back, understand it's final sale and that nothing is charged now, and submit. And they cannot submit anything the maker cannot make.
4.6 Coming-soon PDP (status coming_soon)
Should see:
- Gallery, title, maker line, the "Coming soon" label and
status_note. - The personalization panel with live preview, headed by C17. There is no read-back box, no C4/C5, and no fulfilment fields.
- The waitlist form:
- "Your name".
- "Email (required)".
- An unticked consent checkbox: "Email me once when this item can be ordered." It is required to submit, and it is CASL express consent for that one message.
- The button "Notify me".
- After submit: "Thanks — we'll email you once when {product} opens for orders. That's the only email we'll send about it."
- The submit creates exactly one
tvg_order_requestsrow withkind='waitlist', no items, and consent text and timestamp stored.
Should NOT see: a price, C6 ("Request a proof"), or "Pre-order".
DECIDED 2026-09-28 (default adopted by orchestrator): no full personalized pre-request for coming_soon items at launch — waitlist only.
The "now available" email is founder-pressed only. It is never cron-sent. It is sent once per signup, and its content is defined in tvg-proof-to-pay.md §9, E13.
4.7 Example PDP (status example)
Should see:
- The "Example" label and C3, both near the title and on the hero image.
- The gallery, with every image labelled "Sample image" or "Illustration".
- Title, maker line and description.
- A section headed "Options this item would have" as a plain list, for example: "Name, up to 28 characters · Verse, KJV or WEB · Photo".
- A link: "See gifts you can order now", to
/shop?status=available(or tocoming_soonitems if none are available).
Should NOT see:
- Any price.
- Form inputs, live preview interaction, "Request a proof", "Notify me", or a quantity stepper.
- DECIDED 2026-09-28 (default adopted by orchestrator): no live-preview demo on example pages.
Server: POST /api/tvg/order-requests with this product's id, of any kind, returns 409 {"error":"not_orderable","status":"example"} and creates zero rows.
4.8 Bulk and B2B quote /bulk
State U3. Screenshot: bulk.png.
Should see:
- The heading "Bulk orders for churches, teams and businesses".
- Three plain examples: church gifts (baptism, ordination, volunteer thank-yous); team tumblers with a different name on each; realtor closing gifts with a logo.
- How it works:
- "Tell us what you need"
- "We send a quote and a proof"
- "You approve, pay by card or Interac e-Transfer, and we make it"
- The form:
- "Organization name (required)".
- "Your name (required)".
- "Email (required)".
- "Phone (optional)".
- "Which product?" — a select of
availableproducts plus "Something else". It is pre-filled from?product=. - "How many? (required)" — pre-filled from
?qty=. - "Different names or text on each piece?" (yes/no). If yes: "Paste the list, one per line (up to 500 lines)", with the line count shown. DECIDED 2026-09-28 (default adopted by orchestrator): paste only at launch; no CSV upload.
- "Needed by (optional)".
- Fulfilment, with the same rules as §4.5 (n).
- "Anything else we should know (up to 1,000 characters)".
- C5 adapted as: "I have checked the spelling of every name in my list, and I understand personalized items are final sale unless defective or different from the approved proof." It is required.
- The button "Request a quote", with C7 adapted: "Nothing is charged now. We email you a quote and proof to approve first."
- A direct phone number for B2B buyers who want to talk first. [UNVERIFIED] The front-desk line is not provisioned yet. Show the number only once it rings somewhere real.
Should NOT see:
- "Contact us for pricing" as a dead end: the tier prices for listed products are visible on their PDPs.
- Net-30 invoicing promises (deferred).
- Example or coming-soon products in the product select.
Server: creates one tvg_order_requests row with kind='bulk_quote', organization_name set, one item per selected product (none if "Something else"), and the list stored verbatim in the item's personalization under the key name_list.
DECIDED 2026-09-28 (default adopted by orchestrator): the bulk schema key name is name_list (as used above); quantity need not equal the list length — warn, don't block.
4.9 Guides and policy pages
Guides: /guides/monogram, /guides/photo-engraving, /guides/verses.
- Should see:
- Monogram: the three arrangements with the Mary Ann Jones → M J A example, and the note that conventions vary so the customer chooses.
- Photo: what engraves well, the DPI table in plain words, and real before/after pairs only.
- Verses: KJV vs WEB, the C16 example, and why custom verses need a human check.
- Should NOT see: borrowed Mark & Graham or other stores' text.
Policies: /policies/shipping, /policies/returns, /privacy, /terms, /about. The text comes from a new tvg: section of knowledge/data/policies.yaml (architecture §4 Phase 0), which must exist before these pages ship.
| Page | Must state | Must NOT state |
|---|---|---|
| Shipping | Canada only · $14.50 flat [PLACEHOLDER] · pickup free in Ingersoll by appointment · the delivery window starts at proof approval (F7) · tracked shipments get a tracking email | International shipping, free-shipping thresholds, guaranteed delivery dates |
| Returns & final sale | F6 verbatim, plus the three cases from tvg-proof-to-pay.md §8: maker error → remake or refund; proof differs from your request without being pointed out → remake; your own approved text → final sale. Also how to report a problem, the photo-evidence window, and that non-personalized items (if any are ever sold) follow a separate rule. | "No refunds, ever"; any promise broader than §8 |
| Privacy | What is collected (contact, address, personalization, photos) · photos stored privately, location data removed, deleted 90 days after completion · proof approvals record time, IP address and browser for dispute evidence · no selling of data · the contact for privacy requests | Marketing-list language (there is none) |
| Terms | Division line C1 · BIN (if the founder approves, §3.6) · governing law Ontario [UNVERIFIED — counsel] · pricing in CAD, tax-exclusive · proof-then-pay sequence | GST/HST number labelled with the BIN |
| About | The hub model in plain words, the towns, and the makers link | Headcount, revenue or tenure claims |
4.10 Contact /contact and "Find my request"
Should see:
- Email: DECIDED 2026-09-28 (default adopted by orchestrator):
hello@thamesvalleygifts.ca(the mailbox must exist and be monitored). Phone [UNVERIFIED — front-desk line]. "Pickup in Ingersoll by appointment". - A simple message form: name, email, message (up to 2,000 characters), honeypot. On success: "Thanks — a real person will reply, usually within 1 business day."
- "Find my request": fields for order number (
TVG-######) and email.- On submit, the same message always appears, whether or not they match: "If that order number and email match a request, we've emailed you its link."
- A match sends the request link to the address on file.
- Rate limit: 5 attempts per IP per hour.
Should NOT see:
- A street address for pickup. DECIDED 2026-09-28 (default adopted by orchestrator): do not publish the pickup address; give it in the "ready for pickup" email only.
- Request status shown on this page.
- Any sign of whether an order number exists.
4.11 Request-submitted confirmation
The canonical wording is in tvg-proof-to-pay.md §2.1. This section mirrors it; if the two differ, that spec wins.
Shown immediately after a successful POST (retail or bulk). Screenshot: confirmation.png.
Should see:
- The heading "Request TVG-000123 received". The order number always matches
^TVG-\d{6}$. - "Nothing has been charged."
- "We'll email your proof to {email} within 1 business day. Nothing is made until you approve it."
- The read-back of what they asked for (the same content as §4.5 (m)), with the product, maker, quantity and fulfilment choice.
- IF a photo is still needed: "We still need your photo." with a button "Add your photo" linking to the request page.
- A link: "View your request", to
/request/{token}. - "Didn't get the email in 10 minutes? Check spam, or contact us with your order number."
Should NOT see:
- "Order placed", "Order confirmed", "Thank you for your purchase", "Paid", or a receipt.
- A tax amount or a "total".
- A payment form or pay link.
- Cross-sell products or "Share on Facebook".
Refresh and back:
- Reloading the confirmation does not resubmit.
- Pressing Back returns to the PDP with the form cleared. Values were stored per tab in sessionStorage (§4.12) and are cleared on success.
4.12 Returning customer and draft persistence (U4)
- Form values on a PDP persist in the same browser tab (sessionStorage) so Back/Forward doesn't lose typing. The photo file does not persist. Values are cleared after a successful submit.
- A customer with a request in flight sees the storefront exactly as U1/U2 do. They manage the request only through
/request/{token}, which is defined intvg-proof-to-pay.md. - Submitting a second request for the same product creates a second, separate request. The storefront never merges requests.
5. Server contract: POST /api/tvg/order-requests
This is written as behaviour, so QA can test it without reading the code.
| Case | Response | Rows created |
|---|---|---|
Valid available product, valid personalization, C5 confirmed | 201 {order_number, request_url} | exactly 1 tvg_order_requests + N tvg_order_items + ≥1 tvg_order_events |
| Any text field over its schema limit (for example 25 characters where the limit is 24), sent directly without the browser | 400 {"error":"validation","fields":{"line1":"max_length"}} | 0 |
Unsupported character, a 2-letter monogram, a verse translation not in the field's list, or a verse id not in tvg_verses | 400 with the field key | 0 |
C5 not confirmed (spelling_confirmed false or missing) on retail/bulk | 400 {"fields":{"spelling_confirmed":"required"}} | 0 |
Low-DPI photo without customer_acknowledged_low_res: true | 400 | 0 |
Product example, any kind | 409 not_orderable | 0 |
Product coming_soon, kind retail or bulk_quote | 409 | 0 |
Product coming_soon, kind waitlist, consent ticked | 201 | 1 request (no items) |
| "Opening soon" mode (§3.7), any kind except waitlist | 409 not_open | 0 |
| A client-sent price or total (tampered) | Ignored; the server prices from the schema | Price stored = server price |
| Honeypot filled | 201-shaped response (the bot sees success) | 0 |
Same form submission twice (double-click, retry) using the same Idempotency-Key | Second call returns the first response | exactly 1 |
| More than 10 submissions per IP per 10 minutes | 429 | 0 |
| Shipping address with a non-Canadian province or bad postal code | 400 | 0 |
Stored on success:
personalizationvalues after NFC normalization and trimming,schema_version, aproduct_snapshot, andspelling_confirmed_at.- The final-sale disclosure text version shown (
final_sale_ack_text_version). DECIDED 2026-09-28 (default adopted by orchestrator): the pre-request acknowledgement is stored on the request, as well as at proof approval. Both timestamps feed the dispute evidence intvg-proof-to-pay.md§7.
QA fixtures. The QA checklist creates requests in production. DECIDED 2026-09-28 (default adopted by orchestrator):
- Approve an unlisted QA product (
is_demo_seed=true, statusavailable, a hiddenTVG QAmaker, excluded from/shop, sitemap, catalogue API and payouts). It is reachable by direct URL only. - Add a request flag
is_test(schema addition to architecture §2.2) so test rows never reach payouts or reports.
This is required before the §12 checks that create rows can run on the real host.
6. Accessibility requirements
WCAG 2.2 AA is the bar. Each item is testable.
- Every input has a programmatic
<label>. The limit and required status are inside the label text (§4.5 (d)). - Counters have a visible text form. A polite
aria-liveregion announces the remaining count after the user pauses (debounced to 1 second or more), and at the 80% and 100% thresholds. It does not announce every keystroke. - Errors: inline, icon plus text, linked with
aria-describedby, and naming the field and the fix (WCAG 3.3.1, 3.3.3). On a failed submit, focus moves to the first invalid field. - The preview has alt text restating the content, updated on change. A polite "Preview updated" announcement is debounced. For photo products, C9 is readable text, not an image.
- Keyboard:
- Every control is reachable in visual order.
- Radio groups (translation, arrangement, colour, font, size, fulfilment) use arrow keys.
- The monogram inputs auto-advance but allow Shift+Tab and Backspace across boxes.
- The mobile menu opens and closes with Enter/Space/Escape and returns focus.
- Focus is always visible, with at least a 3:1 outline.
- Reduced motion: with
prefers-reduced-motion: reduce, there are no preview transitions, no smooth scrolling and no animated counters. Content is otherwise identical. - Contrast: text is 4.5:1 or more and UI components 3:1 or more. Status labels are readable without colour (text always present). Sweet-corn yellow is never used as text on white (§3.3).
- Target size: at least 44 × 44 px for steppers, swatches, checkboxes (the clickable label counts) and the primary button.
- Language:
<html lang="en-CA">. - No time limits on the PDP form.
- Zoom to 200% causes no loss of content or function.
7. Mobile rules at 375 px (U1)
- No horizontal page scroll at 375 × 667. Checked with
document.documentElement.scrollWidth <= 375. - Single column, 16 px side gutter.
- The pinned preview: once the shopper scrolls past the gallery into the personalization panel, a compact preview (at most 40% of viewport height) stays pinned at the top, so typing is visible without scrolling back up. It has a "Hide preview" toggle. It is not pinned for photo products (nothing to preview). [INFERENCE, per ux_research §3.1] Validate on a real phone before approval.
- Inputs use a font size of at least 16 px, so iOS doesn't zoom on focus. Email uses
type="email", phonetype="tel", quantityinputmode="numeric", and postal codeautocapitalize="characters". - The "Request a proof" button sits after the read-back and checkbox. It is not a sticky bar that hides the checkbox or the final-sale text. DECIDED 2026-09-28 (default adopted by orchestrator): no sticky bottom bar.
- Filter chips on
/shopscroll horizontally inside their own row (not the page) or collapse into a "Filters" button that opens a sheet. - The confirmation screen puts the order number and "Nothing has been charged." above the fold.
8. Honesty rules
The consolidated list is in §3.4. Additional testable rules:
- Every
exampleitem, everywhere it appears (tile, PDP, maker page,/shop?status=results), shows the "Example" label and C3. There are no exceptions for thumbnails. - No J & B Creations price, photo, review, address or phone appears in HTML, image files, alt text, JSON-LD or the catalogue API response.
- JSON-LD:
Productmarkup has noaggregateRatingorreviewfields.offersappears only foravailableproducts.- Example products carry no
offers.
- A placeholder value (C12's $14.50, and any "Illustration" image) is labelled, and the site can't leave "Opening soon" mode while a [PLACEHOLDER] remains in customer-facing prices.
9. What the storefront shows per user state
This is a quick matrix. The detail lives in §4.
| Surface | U1 phone | U2 desktop | U3 B2B | U4 in-flight |
|---|---|---|---|---|
| Header | Menu button, no cart | Full nav, no cart | Same as U1/U2, and "Bulk orders" is visible | Same as U1/U2; nothing personal |
| PDP form | Stacked, pinned preview | Two columns | Tier table and bulk link prominent when qty ≥ 10 | Blank form (no prefill from earlier requests) |
| Bulk page | Stacked | Two columns | Primary surface | — |
| Find my request | Footer link | Footer link | Footer link | Primary way back besides the email |
10. FOUNDER DECISIONS in this spec
Status: DECIDED = the orchestrator adopted the recommended default on 2026-09-28 (the founder can still override); OPEN = no default existed, or this item is explicitly reserved for the founder, the accountant or counsel.
| # | Decision | Recommended default | Status |
|---|---|---|---|
| D1 | Add an explicit register field to products; are unlisted occasions Main Street? | Yes; yes | DECIDED 2026-09-28 |
| D2 | Keep "Proof by email within 1 business day" (C14) | Keep, or change the string | OPEN — founder |
| D3 | Show BIN 1001760694 publicly | About page only, never labelled GST/HST | DECIDED 2026-09-28 |
| D4 | Confirm the storefront character set against the production fonts | Latin + accents + the listed punctuation | DECIDED 2026-09-28 |
| D5 | Character counting method | NFC, then code points | DECIDED 2026-09-28 |
| D6 | Wording of the couple monogram option | Gender-neutral wording in §4.5 (h) | DECIDED 2026-09-28 |
| D7 | Optional news opt-in | None at launch | DECIDED 2026-09-28 |
| D8 | Full pre-request for coming_soon | No; waitlist only | DECIDED 2026-09-28 |
| D9 | Live-preview demo on example pages | No | DECIDED 2026-09-28 |
| D10 | Bulk list: CSV upload; must quantity equal list length | Paste only; warn, don't block | DECIDED 2026-09-28 |
| D11 | Publish the pickup address | No; send it in the ready-for-pickup email | DECIDED 2026-09-28 |
| D12 | Contact email address | hello@thamesvalleygifts.ca, monitored | DECIDED 2026-09-28 |
| D13 | ChurchWise Gifts self-agreement record | agreement_version='internal' | DECIDED 2026-09-28 |
| D14 | Blocking: Jackie Rayner's written OK for an example profile | Without it, J & B is not shown | OPEN — founder/consent. Interim: placeholder maker cards use "Example maker: a Dorchester sublimation studio" |
| D15 | QA product + is_test flag in production | Yes, needed for §12 | DECIDED 2026-09-28 |
| D16 | Store the pre-request final-sale acknowledgement | Yes | DECIDED 2026-09-28 |
| D17 | Sticky mobile action bar | No | DECIDED 2026-09-28 |
| D18 | Maker-recruitment link | No at launch | DECIDED 2026-09-28 |
| D19 | Shipping rate (replaces the $14.50 placeholder) | — (founder sets) | OPEN — founder (no default exists) |
11. [UNVERIFIED] items in this spec
- iOS HEIC → JPEG conversion with
accept="image/jpeg,image/png,image/webp"(§4.5 (i)). - Carrier (Canada Post tracked) (§4.5 (o)).
- The front-desk phone line exists (§4.8, §4.10).
- Jackie Rayner's consent to an example profile, and her town (§4.4).
- ChurchWiseAI LTD's 9-digit BN for the GST/HST number (§3.6).
- Tax jurisdiction labels outside Ontario pending the accountant (§3.6).
- Mobile pinned-preview behaviour is inference, not observed (§7).
- Ontario as governing law in Terms (counsel) (§4.9).
thamesvalleygifts.caregistered and attached to the Vercel project (architecture R12).
12. QA checklist (run on https://thamesvalleygifts.ca, never on a preview host)
Rules for running it:
- Use Playwright against the real host (host-rewrite middleware is skipped on preview hosts).
- Save artifacts to
knowledge/acceptance/screenshots/tvg-storefront/and the run zip. - "Behaviour" checks sample state at two timepoints and assert a change.
- Every negative check is paired with a positive control, so the command has shown it can find what it looks for.
- Row-creating checks use the D15 QA product only.
12.1 Host and global
-
curl -I https://thamesvalleygifts.ca/returns 200.www.returns a 308 to the apex./tvg/shopon the brand host returns a 301 to/shop. (Owned by WP5; re-checked here.) - Every page in §4 has the exact footer text C1. Positive control: the assertion fails when run against a page fixture with the text altered.
- Header has no element matching /cart|basket|sign in|account/i. Positive control: the same locator finds "Bulk orders".
- Every page root has
data-register. The memorial collection and a memorial PDP givekeepsake; the drinkware collection givesmain_street. - While in "Opening soon" mode: the banner is present and
<meta name="robots" content="noindex">is on every page.
12.2 Status gate
- Example product:
- The PDP shows the "Example" label and the text C3.
getByRole('button', {name: 'Request a proof'})has count 0. Positive control: count is 1 on theavailableQA product.- There is no price text matching
/\$\d/in the main region. - There are no
input/select/textareaelements in the product form region.
- Direct
POST /api/tvg/order-requestsfor the example product id returns 409. The DB count oftvg_order_requestsfor that product is unchanged (read before and after). - Coming-soon product:
- The waitlist form and C17 are present. "Request a proof" count is 0. There is no price.
POSTwithkind:'retail'returns 409.- The waitlist submit with consent creates exactly 1 row with
kind='waitlist'.
- A
draftproduct URL returns 404. Apausedproduct shows C18 and has no form. - Every
/shoptile for an example item contains C3. The count of tiles with C3 equals the count ofexampleproducts listed.
12.3 PDP behaviour (two timepoints)
- The preview changes when text changes.
- Type "Anna" into line 1 and read the preview SVG text: T1 = "Anna".
- Type " Smith" and read again: T2 = "Anna Smith".
- Assert T1 ≠ T2 and T2 contains "Anna Smith".
- Assert the preview
altoraria-labelalso contains "Anna Smith".
- The 25th character is blocked (on a 24-character field):
- Type 30 characters and assert
inputValue().length === 24. The counter reads "24 / 24 (limit reached)". - Paste a 30-character string into the cleared field and assert the length is 24 and the message "Only the first 24 characters fit" is visible.
- Type 30 characters and assert
- Tampered over-limit POST returns 400. Send
POST /api/tvg/order-requestswith a 25-characterline1straight from the test, bypassing the UI. Assert 400 withfields.line1, and the row count is unchanged. - Tampered price:
POSTwithunit_price_cents: 1. The created row's price equals the server price. (Uses the QA product.) - Translation switch:
- Pick Psalm 23:1 with KJV; the preview text contains "The LORD is my shepherd".
- Switch to WEB; it now contains "Yahweh is my shepherd".
- C16 is visible without interaction.
- There is no option containing "NET".
- Monogram: first=M, middle=A, last=J, Traditional. The preview reads "MJA", with the J glyph box larger than the M's. The read-back sentence contains "M J A".
- Photo:
- Upload a 800 × 600 px image on a 6 × 4 in product. The rating text is "Low resolution" and C10 is visible. Submit without ticking C10 shows an error and creates no row.
- Upload a 2400 × 1600 px image: the rating is "Good" and C10 is absent.
- Upload a 20 MB file: the size message appears.
- Final-sale disclosure:
- C4 is visible before the button.
- C5 is unchecked on load.
- Clicking "Request a proof" with C5 unchecked focuses the checkbox, shows "Please confirm you've checked the spelling.", and creates no row.
- Estimated subtotal: read it at qty 1, then set qty 10 and read again. The values differ, and the qty-10 value equals the tier price × 10.
- Quantity over max: set qty to max + 1. The stepper stays at max and the bulk link carries
qty=in its href. - Reduced motion: with
reducedMotion: 'reduce', type text. The preview updates, with no CSS transition or animation on the preview element (computedtransition-durationis 0s).
12.4 Submission
- A submitted request creates exactly one row.
- Read the count of
tvg_order_requestswhereis_test(QA product) = N. - Fill the form validly and double-click "Request a proof".
- Assert the count is N+1, not N+2.
- Assert exactly one
tvg_order_itemsrow for that request. - Assert the confirmation shows
^TVG-\d{6}$, the text "Nothing has been charged.", and no text matching /order placed|paid|purchase/i.
- Read the count of
- Reload the confirmation. The count is still N+1.
- The honeypot filled: the response looks successful and the count is unchanged.
12.5 Accessibility and mobile
- axe-core finds zero serious or critical violations on home, shop, PDP (text, verse, monogram, photo), bulk and confirmation.
- Keyboard only: tab from the page top to "Request a proof" on the text-product PDP. Every control receives visible focus. Arrow keys switch the translation radio.
- The counter live region exists (
aria-live="polite") and contains the remaining count after typing stops. - At 375 × 667:
scrollWidth <= 375on every §4 page. After scrolling into the form, the preview is still in the viewport (bounding box top ≥ 0 and bottom ≤ 0.4 × viewport height). Input font-size is at least 16 px.
12.6 Honesty sweep
- Full-site crawl text search for /review|rated|stars|bestseller|best seller|popular|trending|only \d+ left|people are viewing/i finds 0 matches. Positive control: the same search finds "Example listing" on an example page.
- The JSON-LD on every PDP has no
aggregateRating/review. Example PDPs have nooffers. - The catalogue API and HTML contain no "Forest Grove", "601-3449", "377-1723" or "jbcreations883". Positive control: the search finds "J & B Creations".
13. Decision log (2026-09-28 editorial pass)
Defaulted (DECIDED 2026-09-28, default adopted by orchestrator; the founder can still override): D1, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D15, D16, D17, D18 (§10); the GST/HST-number-via-env-var default (§3.6).
Left open:
- D2 — the C14 "within 1 business day" proof promise: explicitly reserved for the founder.
- D14 — Jackie Rayner's written consent to a public example profile: blocking, reserved for the founder/consent process. Interim rule added: until consent, the J & B page and all J & B example products stay unpublished (
status='onboarding', hidden), and any placeholder maker card in mockups uses the neutral name "Example maker: a Dorchester sublimation studio". - D19 — the actual shipping rate (replacing the $14.50 placeholder): no default exists; the founder must set the real number.
- Governing law (Ontario) in Terms, and the front-desk phone line's existence: unresolved [UNVERIFIED] items, not decision markers with defaults — left as-is.