# 05 TECHNICAL ARCHITECTURE

Placeholders: theme `brand-theme`, plugin `brand-commerce`, function prefix `bc_` / `BC_`, meta prefix `_bc_`, text domain `brand`. See `RENAME.md`.

---

## 1. Repository layout

The repo tracks only the two directories we author, never WordPress core and never other plugins.

```
repo-root/
  CLAUDE.md
  RENAME.md
  README.md
  .gitignore
  .editorconfig
  phpcs.xml
  composer.json          (dev only: phpcs, WordPress and WooCommerce sniffs)
  package.json           (dev only: sass, esbuild)
  docs/
  prompts/
  deploy/
    .cpanel.yml
    deploy-notes.md
  wp-content/
    themes/brand-theme/
    plugins/brand-commerce/
```

### Theme

```
brand-theme/
  style.css                    header only, no styles
  functions.php                loads inc/*, nothing else
  inc/
    setup.php                  theme supports, image sizes, menus
    enqueue.php                asset registry, manifest lookup, versioning
    template-tags.php          bc_breadcrumb(), bc_price_html(), bc_spec_table()
    nav-walker.php
    customizer.php             logo, WhatsApp number, delivery copy, trust badges
    woo-support.php            declare Woo support, gallery, unhook default wrappers
    performance.php            dequeues, defer, preloads, critical CSS injection
  template-parts/
    header/  topbar.php nav-desktop.php nav-mobile.php search-drawer.php
    home/    hero.php category-grid.php shop-by-size.php bestsellers.php
             collection.php why-us.php reviews.php whatsapp-cta.php
    product/ card.php gallery.php spec-table.php includes.php care.php
             delivery-cod.php sticky-atc.php swatches.php
    global/  trust-badges.php empty-state.php pagination.php breadcrumb.php
             filter-drawer.php notice.php
  woocommerce/                 template overrides, minimal, listed in section 4
  assets/
    src/scss/{abstracts,base,components,layout,pages}/  main.scss
    src/js/  main.js filters.js gallery.js quantity.js checkout-city.js
    dist/    main.<hash>.css  main.<hash>.js  asset-manifest.json
    critical/ home.css archive.css single-product.css checkout.css
    img/  fonts/
  front-page.php  index.php  page.php  single.php  404.php
  searchform.php  header.php  footer.php
```

### Plugin

```
brand-commerce/
  brand-commerce.php           bootstrap, guards, HPOS declaration, autoloader
  uninstall.php
  includes/
    class-plugin.php           container, hook registration
    class-activator.php        roles, tables, status flush
    class-installer.php        dbDelta, schema version option
    interfaces/                class-notifier-interface.php  class-courier-interface.php
    order/
      class-order-statuses.php
      class-order-meta.php     typed getters and setters over the CRUD API
      class-admin-columns.php
      class-order-metabox.php  verification, fulfilment, courier, COD panels
      class-order-actions.php  row and bulk actions, AJAX handlers
      class-order-timeline.php writes order notes, the single audit trail
    checkout/
      class-checkout-fields.php
      class-phone.php          PK normalisation and validation
      class-locations.php      province to city dataset from data/
    cod/
      class-cod-gateway.php    extends WC_Payment_Gateway
      class-cod-rules.php      thresholds, blocklist, quantity cap
      class-verification.php   state machine
      class-blocklist.php      phone RTO counter and COD block
    fulfilment/
      class-fulfilment.php
      class-courier-registry.php
      class-courier-manual.php
      class-settlements.php
    payments/
      class-gateway-registry.php
      class-pk-gateway.php        BC_Abstract_PK_Gateway lives here, see the autoload note below
      class-payment-result.php    BC_Payment_Result, the value object handle_callback() returns
      README-adding-a-gateway.md
    admin/
      class-dashboard.php
      class-settings.php
      class-customers.php        customer list and detail, aggregated from bc_order_stats by phone
      class-tools.php            admin-side runners for everything class-cli.php exposes
      class-pages-installer.php  creates the static content pages on activation
      class-login-limiter.php
    notifications/
      class-notifier-registry.php
      class-email-notifier.php
      class-whatsapp-link.php  click-to-chat deep links, no API
    product/
      class-product-spec-fields.php
      class-product-spec-render.php
      class-attributes-installer.php
    integrations/
      class-analytics.php      GA4, Meta Pixel, dataLayer
      class-capi.php           Meta Conversions API, purchase only
      class-schema.php         hand-rolled JSON-LD
      class-feed.php           Merchant Center and Meta catalogue XML
    class-stats-writer.php
    class-cli.php              WP_CLI guarded: seeders, stats rebuild
  assets/admin/  admin.css admin.js
  data/pk-locations.json
  languages/brand-commerce.pot
```

**Autoloader note.** The autoloader maps `BC_Thing` to `class-thing.php`, so every file follows that pattern, interfaces and abstract classes included. `BC_Courier_Interface` lives in `class-courier-interface.php`, `BC_Abstract_PK_Gateway` in `class-pk-gateway.php`. Do not use `interface-*.php` or `abstract-class-*.php` filenames, they will not load.

**The dividing line**: if deleting a file would corrupt or orphan an existing order record, it belongs in the plugin. The theme owns markup, CSS and JS. Nothing else.

---

## 2. Build tooling

Node runs locally and in CI only. The server never runs npm or composer.

- Sass compiles `main.scss` to one minified CSS bundle
- esbuild bundles ES modules to one IIFE targeting `es2018`
- `asset-manifest.json` maps logical names to content-hashed filenames
- `assets/dist/` is committed to the repo

Budgets: CSS under 45 KB minified, JS under 25 KB minified, zero framework.

Cache busting: `bc_asset('main.css')` reads the manifest once per request into a static and returns the hashed URL. Pass `null` as the enqueue version so no `?ver=` query string appears, hashed filenames cache far better on LiteSpeed and any CDN. If the manifest is missing, fall back to `filemtime()`.

Plugin admin CSS and JS are small enough to ship unminified with `filemtime()` versioning.

---

## 3. WooCommerce integration

### HPOS

Enabled from day one, `wp_posts` sync off once initial state settles. Reasoning in decision D-02.

Declare in the plugin bootstrap on `before_woocommerce_init`:

```php
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
    'custom_order_tables', __FILE__, true
);
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
    'cart_checkout_blocks', __FILE__, false
);
```

Rules that follow:

- Order meta only through `$order->get_meta()`, `$order->update_meta_data()`, `$order->save()`
- Never `WP_Query` or `meta_query` against posts for orders. Use `wc_get_orders()`.
- Any raw SQL resolves the table name through `OrdersTableDataStore::get_orders_table_name()`, never a hard-coded string

### Template overrides

Override only where markup demands it. Everything else uses unhook and rehook, because overridden templates rot on every WooCommerce release.

Allowed overrides: `archive-product.php`, `content-product.php`, the `single-product/` partials, `cart/cart.php`, `cart/cart-totals.php`, `checkout/form-checkout.php`, `checkout/form-shipping.php`, `checkout/review-order.php`, `myaccount/` minimal.

Add a WooCommerce template version audit to the QA checklist for every Woo update.

### Key hooks

**Statuses**: `init` for `register_post_status`, `wc_order_statuses` for labels, `woocommerce_order_is_paid_statuses` for `wc-completed` only (see the data model, do not add Shipped), the `woocommerce_excluded_report_order_statuses` and `woocommerce_actionable_order_statuses` **options** for WooCommerce Analytics bucketing (the `woocommerce_reports_order_statuses` filter only affects the deprecated legacy reports), `bulk_actions-woocommerce_page_wc-orders` and `handle_bulk_actions-woocommerce_page_wc-orders` for bulk transitions. Emails via a `WC_Email` subclass on `woocommerce_email_classes` plus `woocommerce_email_actions`.

**Checkout**: `woocommerce_checkout_fields` to restructure, `woocommerce_after_checkout_validation` for phone and address validation, `woocommerce_checkout_create_order` to write meta onto the order object, `woocommerce_checkout_process` for COD rule enforcement.

**Admin columns**: `woocommerce_shop_order_list_table_columns` to add the column, and the **action** `woocommerce_shop_order_list_table_custom_column` (which receives `$column_id` and the `WC_Order` object) to render it. There is no `..._column_{id}` filter under HPOS, do not look for one. `woocommerce_order_list_table_restrict_manage_orders` for the fulfilment and courier filter dropdowns, `woocommerce_order_list_table_prepare_items_query_args` to translate those into queries.

---

## 4. Payment gateway abstraction

`BC_Abstract_PK_Gateway extends WC_Payment_Gateway` defines the contract:

```php
abstract public function build_payment_request( WC_Order $order ): array;
abstract public function handle_callback( array $payload ): BC_Payment_Result;
abstract public function verify_signature( array $payload ): bool;
abstract public function process_refund( $order_id, $amount = null, $reason = '' );
```

The abstract class implements everything that never changes: writing `_bc_payment_*` meta, calling `$order->payment_complete( $txn_id )`, status mapping, idempotency keyed on transaction ID, and one shared REST callback route at `brand/v1/payment/(?P<gateway>[a-z0-9-]+)/callback`.

`BC_Gateway_Registry` collects subclasses via the `bc_payment_gateways` filter and feeds `woocommerce_payment_gateways`.

Nothing provider-specific ships at MVP. Adding Safepay, PayFast, JazzCash or Easypaisa later is one file plus a settings array. Credentials live in `wp-config.php` constants, never in options, never in the repo. No card data is ever received.

---

## 5. Namecheap shared hosting

### Cron

```php
define( 'DISABLE_WP_CRON', true );
define( 'WP_CRON_LOCK_TIMEOUT', 60 );
```

cPanel cron every 5 minutes:

```
*/5 * * * * cd /home/USER/public_html && /usr/local/bin/php wp-cron.php >/dev/null 2>&1
```

WP-Cron on a low-traffic store fires unpredictably, and on a traffic spike it fires on every request and burns the entry process limit.

Batch work goes through Action Scheduler, which ships with WooCommerce. Shorten retention via `action_scheduler_retention_period` to 14 days or its tables become the largest in the database.

### LiteSpeed Cache

- Public cache on, TTL 604800. Logged-in cache off.
- Do not cache: `/cart`, `/checkout`, `/my-account`, `/wc-api`, `/track-order`
- Exclude cookies `woocommerce_items_in_cart` and `woocommerce_cart_hash`
- Browser cache TTL 31536000 for static assets
- Minify on. **Combine off.** LiteSpeed's combine breaks WooCommerce variation JS regularly.
- **UCSS off.** It strips WooCommerce's dynamically added classes. Use our hand-written critical CSS instead.
- Guest Mode and Guest Optimization on, this matters for first-time Meta ad traffic
- QUIC.cloud image optimisation on for WebP
- **Crawler: expect it to be unavailable.** Namecheap shared plans disable the LSCache crawler at the server level, and the plugin will show it as disabled by the server administrator. Do not build the cache-warming strategy around it. Warm the cache instead with a cPanel cron running a short PHP script that `wp_remote_get`s the top 100 URLs from the sitemap, one every two seconds, nightly.
- Object cache only if the host exposes LSMCD, otherwise off

Turn the aggressive options on one at a time and retest checkout after each.

### No object cache

Reduce query volume instead:

- `WP_POST_REVISIONS` 5, `EMPTY_TRASH_DAYS` 7
- Audit autoloaded options monthly: `SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload='yes'` must stay under 800 KB
- Transients with 12 hour expiry for best sellers, category counts and homepage rails, purged on `save_post`
- `posts_per_page` capped at 12 on archives
- HPOS keeps orders out of `wp_posts`, which is most of the win

### PHP and limits

Request via cPanel MultiPHP INI Editor: PHP 8.2, `memory_limit` 512M, `max_execution_time` 120, `max_input_vars` 5000.

**These are requests, not guarantees.** Shared plans cap `memory_limit` and `max_execution_time` at the host level, and a WordPress constant above the enforced PHP ceiling does nothing. `max_input_vars` matters most: the default is 1000, and a variable product with many variations exceeds it, at which point the save **silently truncates** and variations vanish with no error. Verify the value actually took effect with `phpinfo()` before building variable products, and if it did not, raise a support ticket. Do not treat this as a checkbox.

```php
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
```

Treat these as ceilings, not guarantees. The real constraints are Entry Processes (typically 20) and I/O. Anything that spawns loopback HTTP requests will exhaust them.

### Deployment

Primary: cPanel Git Version Control with a `.cpanel.yml` that copies only the two tracked directories into `wp-content`. cPanel's git has no npm and no composer, which is why `dist/` is committed.

Fallback: scripted SFTP push of the two directories only, never a full-site upload.

Emergency: zip upload via File Manager.

Never edit files through the WordPress theme or plugin editor. `DISALLOW_FILE_EDIT` is set to true.

Database changes go through `class-installer.php` and its schema version option, never by importing a SQL dump onto production.

### Staging

A `staging.` subdomain in the same cPanel account, its own database, `WP_ENVIRONMENT_TYPE = 'staging'`, forced `noindex`, cPanel password protection, gateways in test mode, emails intercepted. It shares the account's CPU and inode quota, so keep it small and never load-test there.

### Known pitfalls

- PHP `mail()` from a shared IP lands in spam. Configure SMTP on day one.
- Saving permalinks rewrites `.htaccess` and can drop LiteSpeed rules. Keep a copy.
- The inode limit, not disk size, is what a Namecheap shared account actually runs out of. Every extra registered image size multiplies inodes across the catalogue.
- cPanel's own backups are not a strategy. Use offsite.

---

## 6. Approved plugin list

Installing anything not on this list requires a decision log entry first.

| Plugin | Why |
|---|---|
| WooCommerce | the platform |
| LiteSpeed Cache | server-level caching plus QUIC.cloud WebP, the single largest performance win on this host |
| The SEO Framework | titles, descriptions, canonicals, sitemap, an editable meta box for the client. Its schema output is disabled, we hand-roll JSON-LD. |
| WP Mail SMTP | order emails must actually arrive |
| UpdraftPlus | offsite backups with a tested restore |
| Two-Factor | admin 2FA, tiny, maintained by the WordPress core team |
| Password Policy Manager, or a 20-line custom rule | brief section 34 requires a strong password policy. A `user_profile_update_errors` and `validate_password_reset` rule enforcing 12 characters with mixed classes is smaller than any plugin, prefer that. |
| Safe SVG | only if the brand supplies SVG icons through the media library |

### Explicitly rejected

- Elementor, WPBakery, Divi, any page builder. The brief forbids it and they destroy the performance budget.
- Jetpack. Enormous, and duplicates what we already have.
- WooCommerce product filter plugins. Our own filter over `pa_` attributes is cheaper and adds no joins.
- WPML, Polylang. Urdu is a future requirement. Build translation-ready and add it when it is real.
- Contact Form 7, WPForms. One contact form is a shortcode and `wp_mail()`.
- Any COD OTP or abandoned cart plugin. That is business logic and belongs in `brand-commerce`.
- Yoast or Rank Math alongside The SEO Framework. Never two SEO plugins.
- WooCommerce Blocks checkout. Declared incompatible on purpose.
- Slider Revolution and sliders generally. The hero is a static image with a link.
- WP Rocket, W3 Total Cache, or any second caching plugin. They conflict with LiteSpeed at the `.htaccess` level.
- Order status manager plugins. Statuses are registered in code.
- Wordfence and similar. Their file scanners trigger the host's CPU limits on shared hosting.

---

## 7. Security implementation

### wp-config.php

```php
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('WP_AUTO_UPDATE_CORE', 'minor');
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('DISABLE_WP_CRON', true);
define('WP_POST_REVISIONS', 5);
define('EMPTY_TRASH_DAYS', 7);
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
define('CONCATENATE_SCRIPTS', false);
define('AUTOSAVE_INTERVAL', 120);
define('WP_HOME', 'https://DOMAIN');
define('WP_SITEURL', 'https://DOMAIN');
```

Plus freshly generated salts and a random table prefix, not `wp_`.

### Server and cPanel

Force HTTPS with HSTS once SSL is verified. Ask support to confirm `disable_functions` covers `exec, passthru, shell_exec, system, proc_open, popen`. This is `PHP_INI_SYSTEM`, so it cannot be set from MultiPHP INI Editor or a user `php.ini`. It is a support ticket, not a configuration step, and on most Namecheap shared plans it is already set. `Options -Indexes`. Deny PHP execution in `/wp-content/uploads/` via a local `.htaccess`. Block `xmlrpc.php`, `readme.html`, `wp-config.php`, `.git`, `*.sql`, `*.log`. Permissions 644 files, 755 directories, `wp-config.php` 600. A dedicated MySQL user with only the grants it needs. cPanel two-factor on. One FTP account per developer, deleted at handover.

### Login protection

Custom transient-based rate limiter in `BC_Login_Limiter`: five attempts per IP per fifteen minutes. `xmlrpc_enabled` false and blocked in `.htaccess`. User enumeration disabled (`?author=1` redirect, REST `/wp/v2/users` restricted to logged-in). Generic login error messages. Two-Factor on every admin account. One administrator, everyone else Shop Manager. Admin sessions expire after 8 hours.

### Code rules

The sanitise and escape table, the nonce plus capability rule, and the AJAX pattern are in `CLAUDE.md` section 6. They are binding.

Review photo uploads are deferred (D-21), so there is no public write path in MVP. If that changes, uploads go through `wp_handle_upload` with an explicit mime whitelist and `wp_check_filetype_and_ext`.

---

## 8. Backup and recovery

Backed up: `/public_html` (theme, plugin, uploads, wp-config) plus a full MySQL dump. Code also lives in a private Git repo, which is the real source of truth for code.

UpdraftPlus, database every 6 hours, files daily. Retention 14 database and 7 file copies. Destination Google Drive or Dropbox on a dedicated brand account, never the same cPanel account, because a hacked or suspended host takes local backups with it.

Second layer: a weekly `mysqldump` via cPanel cron to `/backups` outside `public_html`, gzipped, keep 4.

Manual full backup before every plugin update, theme deploy and WooCommerce major version.

**Restore testing** before launch and monthly after. Restore the newest set into a fresh staging database, run a URL search-replace, then verify: homepage renders, product page renders with images, cart adds, checkout loads with COD selectable, a test order places, admin login works, the order list shows restored orders, media thumbnails resolve, permalinks resolve after a flush, and the last order number matches production. Record the wall-clock time. That number is the recovery time to quote the client. Target under 60 minutes, with a 6 hour recovery point.
