# 07 DECISION LOG

Decisions taken in the brainstorm session, locked before development starts. Each entry says what was decided, why, and what was rejected. Where the decision overrules the client brief, that is stated plainly.

Add a new numbered entry whenever a decision changes during the build. Never edit history, append.

---

## D-01 Theme approach: custom classic theme, built from scratch

Hand-written PHP templates with WooCommerce template overrides, no parent theme, no page builder, no block theme.

Rejected: GeneratePress or Blocksy child theme (vendor CSS and JS we would spend the build fighting), full site editing block theme (WooCommerce block templates are still awkward to control precisely, and the design in this brief needs precision).

Consequence: more upfront work, complete control of the performance budget, nothing to un-style.

## D-02 HPOS is enabled from day one

WooCommerce High Performance Order Storage on, `wp_posts` sync off once the initial state settles.

Reasoning: the order list is the heaviest admin query in this build, and this store filters and sorts orders by custom meta constantly. On shared MySQL with no object cache, `postmeta` self-joins are the first thing that will time out. HPOS is also WooCommerce's forward direction, so starting on legacy storage only buys a migration later.

Consequence: `get_post_meta` and `WP_Query` are banned for orders. Everything goes through the CRUD API and `wc_get_orders()`.

## D-03 Blocks checkout declared incompatible

The plugin declares `cart_checkout_blocks` incompatible and the store uses the shortcode cart and checkout.

Reasoning: every custom checkout field, the Pakistani city selector, the phone validator and the COD rules hook into the classic checkout. Supporting both doubles the work for zero MVP benefit.

Revisit when WooCommerce forces the issue, which is a post-MVP migration, not an MVP concern.

## D-04 Order statuses reduced from thirteen to eight, three of them custom

**This overrules section 15 of the brief.**

The brief specifies a nine-stage chain plus four terminal states. Every WooCommerce status costs an email template, an admin filter, a report bucket, a stock decision and staff confusion. A status earns its place only if it changes what the business does next, or how money is counted.

Final set:

| Slug | Label | Origin |
|---|---|---|
| `wc-pending` | Pending Payment | native, online gateway only |
| `wc-bc-verify` | Verification Required | custom |
| `wc-processing` | Confirmed | native, relabelled |
| `wc-bc-shipped` | Shipped | custom |
| `wc-completed` | Delivered | native, relabelled |
| `wc-cancelled` | Cancelled | native |
| `wc-bc-rto` | RTO / Returned | custom |
| `wc-refunded` | Refunded | native |

Demoted to meta fields, still filterable in the order list:

- Packed, Ready for Courier, Dispatched become `_bc_fulfilment_status`.
- Out for Delivery becomes `_bc_delivery_status`, a courier fact rather than a commercial state.
- COD Collected becomes `_bc_cod_status`.
- Settlement Received becomes `_bc_settlement_id` plus a settlement record.

Reusing `wc-processing` as Confirmed rather than creating a parallel status keeps WooCommerce's own stock reduction, paid-status logic and Analytics working without rewiring. Same for `wc-completed` as Delivered.

## D-05 RTO is a first-class status, Cancelled is not the same thing

RTO cost real money in courier charges. Cancelled did not. Reporting must be able to tell them apart, and gross margin is dishonest if RTO charges are not recorded against the order.

## D-06 Customer identity is keyed on the normalised phone number

**This overrules section 24 of the brief**, which assumes a customer record exists.

Section 24 of the brief wants total orders and total spent per customer, but guest checkout produces no WordPress customer record, and email is frequently absent or fake in this market.

Decision: `_bc_customer_phone` stores the canonical `03XXXXXXXXX` form on every order, indexed via the stats table. Customer history, RTO history and the COD blocklist all key on that number.

## D-07 COD verification is threshold-driven, not verify-everything

Three bands, all admin-configurable, not hard-coded:

- Under the low threshold: auto-confirm, single WhatsApp confirmation message.
- Between low and high: mandatory verification before confirmation.
- Above the high threshold: verification plus optional partial advance, cancel after 48 hours if unreachable.

Verification is also forced regardless of value when any of these fire: line quantity above 5, prior RTO or cancellation on the same phone, delivery city not recognised, address under 25 characters.

Suggested launch values Rs 5,000 and Rs 20,000, to be confirmed by the client (see open questions).

## D-08 The phone blocklist ships in MVP

Two RTOs on one phone number disables COD for that number and shows advance payment only.

Reasoning: Pakistani COD RTO commonly runs 15 to 30 percent on cold ad traffic. This is a few hours of work and it is the highest return per hour in the entire build.

## D-09 Two custom tables, both justified, neither a source of truth

`{prefix}bc_order_stats`: one denormalised row per order, written on status transition. It exists because the ops dashboard needs daily aggregates and running those over `wc_orders_meta` on shared MySQL with no object cache is the query that will fall over first. Fully rebuildable from orders, so it is a cache. **The rebuild must be reachable from a button in wp-admin, not only from WP-CLI**, because Namecheap shared plans frequently have no SSH.

`{prefix}bc_settlements`: a settlement is one courier remittance covering many orders. There is no WooCommerce equivalent, and modelling it as order meta makes reconciliation impossible.

Rejected tables: verification log (order notes carry the audit trail), courier events (meta plus notes suffice at manual scale), custom customer table (WooCommerce lookup tables plus the phone key cover it).

## D-10 Attributes: filterable becomes a global attribute, descriptive stays as meta

Global `pa_` attributes with terms: `pa_size`, `pa_color`, `pa_fabric`, `pa_pattern`, `pa_season`, `pa_set-type`. Only `pa_size` and `pa_color` are used for variations.

Descriptive free-form fields stay as product meta: dimensions, pieces, includes, care, thread count. Turning "230 x 250 cm" into taxonomy terms produces thousands of single-product terms and wrecks `term_relationships`.

## D-11 Build tooling runs locally, never on the server

Sass plus esbuild via npm scripts. `assets/dist/` is committed. Cache busting by content hash in `asset-manifest.json`, with `null` passed as the enqueue version so no query string appears.

Reasoning: cPanel has no npm and no composer on the deploy path. The server receives artefacts.

## D-12 URL structure: category base stripped, product base becomes `/shop/`

Categories: `/bed-sheets/cotton/`. Products: `/shop/cotton-double-bedsheet-floral/`.

Rejected: fully stripping the product base, which risks top-level collisions with pages and categories and forces expensive rewrite fallbacks. `/shop/` costs nothing in ranking and keeps the rewrite table safe.

Rejected: category path inside the product URL, which creates duplicate paths when a product sits in two categories.

Mandatory: a full URL crawl on staging before cutover, and the ability to revert to the default bases as a one-setting rollback.

## D-13 SEO plugin for meta, hand-rolled JSON-LD for schema

The SEO Framework handles titles, descriptions, canonicals and the sitemap, and gives the client an editable meta box. Its own schema output is disabled.

Product, BreadcrumbList, Organization and WebSite JSON-LD are hand-rolled in `brand-commerce`, because Merchant Center free listings need `shippingDetails` and `hasMerchantReturnPolicy` that plugin output does not reliably provide.

Rejected: Yoast and Rank Math, both heavier than needed here. Rejected: two SEO plugins at once, ever.

## D-14 Faceted filter URLs are noindex, follow

Filter and sort parameters get `noindex, follow` plus a canonical to the clean category. Paginated archives self-canonicalise with a `| Page N` title suffix.

**They are deliberately left crawlable.** See D-25 for why a `robots.txt` disallow would defeat this.

Reasoning: filters on six attributes generate a combinatorial index flood. This is the most common way a WooCommerce store poisons its own Search Console coverage.

## D-15 Cart fragments are dequeued site-wide

`wc-cart-fragments` fires an uncached `admin-ajax` POST on every page load and is the single largest performance killer on shared hosting.

Replacement: the header cart count renders server side, and a single tiny REST call to `/wp-json/brand/v1/cart-count` fires on `DOMContentLoaded` only when a `woocommerce_cart_hash` cookie exists. Empty-cart visitors, which is most ad traffic, make zero extra requests.

## D-16 Analytics without a tag manager

`gtag.js` for GA4 and the Meta pixel loaded directly from the plugin, no GTM container (90 KB plus for no MVP benefit). A `dataLayer` is still pushed for future portability.

Meta Conversions API: yes, but only the server-side `purchase` event, fired from `woocommerce_thankyou` with hashed phone and email and a matching `event_id` for deduplication. Roughly 80 lines, and it recovers most iOS attribution loss on COD orders. Full CAPI coverage is a future feature.

`purchase` is guarded by an order meta flag so a thank-you page refresh cannot double count.

## D-17 Security: no heavy security plugin

Two-Factor (the core team plugin, tiny) for admin 2FA, plus a transient-based login rate limiter in `brand-commerce`, plus the wp-config hardening set.

Rejected: Wordfence and similar. Their file scanners are a CPU killer on shared hosting and will trigger the host's resource limits.

## D-18 Email goes through an SMTP relay from day one

PHP `mail()` from a shared IP lands in spam, which silently destroys a COD business. WP Mail SMTP with Brevo or Amazon SES, plus SPF, DKIM and DMARC records, plus a WhatsApp confirmation on every order so email is never the only channel.

## D-19 Homepage cut from fourteen sections to eleven

**This overrules section 7 of the brief.**

Cut: standalone Primary CTA (belongs in the hero, a separate band under the hero is dead scroll), Shop by need/occasion (needs curated merchandising that will not exist at launch), Promotional section (with no real promotion it becomes filler or invented urgency, which the brief itself bans), Social proof as a separate block (same job as Customer reviews, splitting it gives two thin sections instead of one convincing one), Newsletter (email capture converts poorly against WhatsApp in this market).

Added: **Shop by size**, a four-chip row for Single, Double, Queen, King. Size is the first constraint a bedding buyer has, and surfacing it removes the largest source of wrong-variation orders and returns.

Net: roughly 5,200px of mobile scroll instead of 7,600px, and every remaining section has a job.

## D-20 Payment gateway abstraction now, integration later

`BC_Abstract_PK_Gateway` defines the contract (`build_payment_request`, `handle_callback`, `verify_signature`, `refund`) and implements everything that never changes: payment meta, `payment_complete()`, status mapping, idempotency by transaction ID, and one shared REST callback route.

Adding Safepay, PayFast, JazzCash or Easypaisa later is one file implementing four methods plus a settings array. No changes to checkout, order meta or reporting.

Nothing provider-specific ships now, because building against a PSP before merchant approval is dead work.

## D-21 Deferred to v2, explicitly

**This overrules sections 25, 27 and 31 of the brief** on customer accounts, photo reviews, and the marketing-cost analytics respectively.

Customer accounts and dashboard (guest checkout plus an order-lookup page by order number and phone covers the real need), photo reviews (upload path, moderation and inodes on shared hosting), product video as an upload (YouTube or Vimeo facade embeds only, never the media library), filtering by pattern and availability (the product data will not exist at launch), coupon usage limits per user and category restrictions, CAC and ROAS and contribution margin analytics (the cost data is not in the system, GA4 plus Pixel with correct purchase events is the whole MVP requirement), Urdu localisation (strings translatable, ship English), a factory portal in any form, courier API integration, and separate Store Manager staffing unless a second person is hired before launch.

## D-22 Hosting has a stated upgrade trigger

Shared hosting is a launch-quality risk, not a cost saving. No object cache means every anonymous page must be served from LiteSpeed's page cache or the site is slow, and cPanel entry-process limits produce 508 errors under exactly the traffic a successful Meta campaign brings.

Trigger, agreed before launch rather than discovered during a campaign: **at 40 orders per day, or the first 508 error under ad traffic, move to a VPS with Redis.** Cloudflare free sits in front from day one to absorb edge traffic and filter bots.


## D-23 Product feed and ItemList schema are additions, and here is why

Neither is in the brief. Both are added deliberately.

The feed exists because launch traffic is Meta ads, and dynamic remarketing needs a catalogue. The same file serves Google Merchant Center free listings, which is meaningful organic reach for a Pakistani retailer at no media cost. Roughly a day of work for a channel that would otherwise be closed.

`ItemList` on category pages is four lines of JSON-LD and improves how category results render.

Both are in scope. Everything else not in the brief still needs an entry here before it is built.

## D-24 Staging exists from phase 00, not from phase 15

Phases 12, 13 and 14 need a real host: LiteSpeed behaves nothing like a local server, URL rewrites have to be crawled somewhere real, and a backup restore cannot be tested against a Docker container.

So the staging subdomain is provisioned in phase 00 alongside the local environment, and the local environment is for fast iteration only. Every phase that touches caching, URLs, email or backups is verified on staging, not locally.

## D-25 Filter URLs are noindex and crawlable, not disallowed in robots.txt

An earlier version of this package said both. That combination does not work: a URL disallowed in `robots.txt` is never fetched, so the crawler never sees the `noindex` or the canonical, and any filter URL already in the index stays there as an untitled entry.

Locked: filter and sort URLs carry `noindex, follow` plus a canonical, and are **crawlable**. `robots.txt` disallows only `/cart/`, `/checkout/`, `/my-account/` and `/*?add-to-cart=`, none of which need to be de-indexed by a meta tag.

## D-26 Static content pages are part of MVP, not an afterthought

Size Guide, Delivery and Payment, Returns and Exchange, About, Contact, Privacy and Terms are linked from the footer, the mobile drawer and the product page. A dead link in the footer of a COD store is a trust failure, and Returns and Size Guide are both read before a purchase decision in this category.

They are built in phase 11 with real content from the client, not lorem ipsum. If the client copy has not arrived, the page ships with a short honest placeholder and an entry in the open questions, never a broken link.
