Repository navigation
Release 1.4.0 - #56
Merged
Merged
Conversation
Introduce LuziApi\Support\Wp, a small stateless helper that converts the mixed values returned by WordPress/WooCommerce (wpdb rows, get_meta(), superglobals) into strict types without changing behaviour: - int()/str() replace (int)/(string) casts on mixed scalars; - row()/rows() normalise wpdb ARRAY_A results into array<string, mixed>; - prepared() wraps wpdb::prepare(), centralising the single trusted internal-table-name literal-string exception and always returning a string. This is the building block used to resorb the PHPStan baseline at level max.
Resolve the level-max errors in the Pilotage repositories, WooCommerce adapters, service provider and history projector: wpdb prepare()/rows typed through the Wp helper, WooCommerce getters guarded with instanceof, and honest PHPDoc array shapes on the customer history projector. No behaviour change.
Resolve the level-max errors in the loyalty ledger and identity links, the order-tracking access/session/history repositories and the tracking page controller: wpdb prepare()/rows and superglobal reads typed through the Wp helper, WP_Comment guarded with instanceof, honest PHPDoc on the loyalty view. No behaviour change.
Resolve the level-max errors in the pilotage admin controllers and the shared SurfacesActionErrors trait: superglobal ($_GET/$_POST) reads typed through the Wp helper (absint kept where it clamped), the WooCommerce submenu access guarded with is_array, wp_date() string|false results coerced, plus the mailer handler's formatted record. No behaviour change.
Resolve the level-max errors in the inc/ procedural files and page.php: meta, option and superglobal reads typed through the Wp helper, WooCommerce objects (WC_Product, WC_Email, WC_Shipping_Rate, WC_DateTime) guarded with instanceof, array<mixed> shipping/offer structures rebuilt via typed locals, and wp_date()/wp_json_encode() string|false results coerced. No behaviour change.
The whole theme passes PHPStan level max, so the debt-freezing baseline is no longer needed: delete phpstan-baseline.neon, remove its include from phpstan.neon.dist and update CONTRIBUTING to state that max must stay green by fixing the underlying type rather than adding a baseline or ignore. Relates to: #13
Resorb the PHPStan baseline (theme clean at level max)
Purely structural rename, no behaviour change: the admin/order module becomes the core "Shop" bounded context ahead of the hexagonal/DDD migration of inc/ (issue #1). - namespace LuziApi\Pilotage\ -> LuziApi\Shop\ across src/, inc/, functions.php and the 17 tools/e2e scripts - src/Pilotage/ -> src/Shop/ and tests/Pilotage/ -> tests/Shop/ (test namespace LuziApi\Tests\Pilotage -> LuziApi\Tests\Shop) - composition-root class PilotageServiceProvider -> ShopServiceProvider Admin-facing labels, Twig view paths (admin/pilotage/) and the admin-pilotage.js asset are left unchanged. The internal layer reshape (3 layers, Domain repositories/gateways, Shared kernel) and the Loyalty->Shop cross-context leak fix are deferred to the issue #1 PR. Gates green: PHP-CS-Fixer, PHPStan (max), PHPUnit (272 tests) and the e2e suites (vente, vente+loyalty, loyalty, receipt, orders). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
* docs(architecture): add ADR 0001 — hexagonal + CQRS conventions Freezes the target architecture for the inc/ migration (issue #1), aligned on the hellobees project: three bounded contexts (Shop core, Loyalty, Newsletter) plus a Shared kernel; three layers only (Domain/Application/Infrastructure, no UserInterface/Bootstrap folders, composition root as a single ServiceProvider); CQRS handlers that return their result (no presenter); typed aggregate ids; repository + Criteria + Paginator in the domain; native exceptions via webmozart/assert; ACL gateways for cross-context references. * refactor(shared): unify the Clock port and adapter into a Shared kernel Establishes src/Shared and removes real triplication: the three per-context Clock ports (Shop, Loyalty, OrderTracking) and their three WordPressClock adapters collapse into one Shared\Domain\Clock (now + timezone, the superset) and one Shared\Infrastructure\WordPress\ WordPressClock. No behaviour change. Loyalty/OrderTracking test doubles gain the timezone() method to satisfy the superset interface. Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (receipt, loyalty, tracking). * refactor(shared): promote the Money value object into the Shared kernel Moves Money out of Shop/Domain/Shared (a "Shared" folder nested inside a context) to the real kernel at Shared/Domain/ValueObject/Money, and its test to tests/Shared. Pure move, no behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (vente, vente-volume). * refactor(shared): absorb the Support helpers into the Shared kernel Moves src/Support (the Wp WordPress/`$wpdb` typing helper used across the infrastructure adapters, and WordPressMailerHandler) into Shared/Infrastructure; the LuziApi\Support namespace disappears. Pure move, no behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (customer-profile, receipt). * refactor(shop): reshape to the three-layer layout Aligns Shop with the ADR 0001 layout (Domain/Application/Infrastructure, no UserInterface/Bootstrap folders): - Application/Port/* (all system or cross-context gateways — the real aggregate repositories already live in Domain) -> Domain/Gateway/ - UserInterface/Admin/* -> Infrastructure/WordPress/Admin/ - Bootstrap/ShopServiceProvider -> Infrastructure/ShopServiceProvider Domain keeps its per-subdomain module grouping (Sales, Customer, Receipt, Inventory, Activity), a legitimate DDD organisation. Pure structural move, no behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (vente, merge-admin, offered-pot, exclusion). * refactor(shared): promote NormalizedPhone to the Shared kernel Removes the last Loyalty -> Shop cross-context leak: LoyaltyIdentity imported Shop\Domain\Customer\NormalizedPhone. The phone-normalisation value object is generic identity material, so it moves to Shared/Domain/ValueObject/NormalizedPhone; Shop and Loyalty now both depend on the kernel (allowed) instead of Loyalty reaching into Shop. Gates green: CS-Fixer, PHPStan (max), PHPUnit (272), e2e (customer-profile, identity-links, merge-admin). * refactor(shop): extract the free-delivery zone rule into the domain First inc/ business-rule migration (issue #1). The "free delivery = France + 37150 + Bléré/Luzillé" rule moves out of inc/order-workflow.php into Shop\Domain\Sales\DeliveryDestination, a WordPress-free value object with self-contained city normalisation. luziapi_is_local_delivery_destination becomes a thin WP adapter that delegates to the value object; luziapi_normalize_city (used only internally) is removed. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (287, +15 new), e2e (delivery-zone). * refactor(shop): extract fulfillment-mode and order-source rules into the domain Second inc/ business-rule migration (issue #1): - FulfillmentMode: the shipping-method -> mode mapping (free_shipping = delivery, local_pickup = pickup) and the mode/intermediate-status consistency rule (out-for-delivery excludes pickup, ready-for-pickup excludes delivery). - OrderSource: the sales-channel reference list and the source resolution (explicit source wins; legacy checkout/store-api orders read as "online"). Both are WordPress-free and keep returning the same strings, so every caller (inc/, admin controllers, order repository, tracking gateway) is untouched. The inc/order-workflow.php functions become thin WC adapters. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (308, +21 new), e2e (orders workflow: delivery/pickup detection + source). * refactor(shared): extract the French business-day calendar into the domain Third inc/ business-rule migration (issue #1). The French public-holiday computation (fixed days + Easter-based days), the business-day test and the add-business-days arithmetic — used to compute the BACS payment deadline and reminder — move from inc/payment-deadline.php into Shared\Domain\BusinessCalendar, WordPress-free and reusable. luziapi_add_business_days becomes a thin delegate; the internal holiday/business-day helpers (no other callers) are removed. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (317, +9 new), e2e (orders: BACS deadline scheduled on-hold, unscheduled once paid). * refactor(shop): extract the order-status email content into the domain Fourth inc/ business-rule migration (issue #1). The per-status customer email content — message lines, step label and closing line — moves out of the WC_Email subclass into Shop\Domain\Notification\ OrderStatusEmailContent, a WordPress-free value object built from already-resolved context (order number, formatted deadline, cancellation reason, pickup address). The Luziapi_Order_Status_Email class keeps the WooCommerce wiring (templates, hooks, sending, loyalty summary) and delegates the content through a small email_content() helper. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (324, +7 new), e2e (orders workflow emails, vente + loyalty completed email). * refactor(shop): extract legal-document versioning and withdrawal validation Fifth inc/ business-rule migration (issue #1): - LegalDocument: the versioned legal document (current version, label, immutable per-vintage PDF) and the PDF selection rule for the version a customer accepted (format check + fallback to the current version, so a dubious stored value never builds a file path). CGV now uses it. - WithdrawalRequest: the withdrawal-form field validation and the scope label. Email-format validation stays with the WP host (is_email) and is passed in as a bool, keeping the value object WordPress-free. The inc/commerce-legal.php functions become thin adapters. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (335, +11 new), e2e (orders: CGV PDF attached to the first confirmation e-mail). * refactor: guard the domain against WordPress and align CONTRIBUTING with ADR 0001 Closes the inc/ migration groundwork (issue #1): - Architecture test: fails if any src/*/Domain/ file references WordPress or WooCommerce (wp_/wc_/$wpdb/get_option/hooks/WC_*/WP_*), locking in the framework-free domain established by this PR. - CONTRIBUTING.md updated to the three-layer layout — no UserInterface/Bootstrap folders; inbound adapters and the composition-root ServiceProvider live in Infrastructure; ports live in Domain/Repository and Domain/Gateway; handlers return their result (no presenter) — and references ADR 0001. functions.php already boots each context's provider and stays minimal. Gates green: CS-Fixer, PHPStan (max), PHPUnit (336, +1 guard). * refactor(shop): address check-pr findings (delivery parity, email guard, tests) Following the automated review of the inc/ migration: - DeliveryDestination now receives an already-normalised city; the WordPress normalisation (sanitize_text_field + remove_accents) moves back to the inc/ adapter, restoring exact parity with develop. The domain keeps only the FR + 37150 + Bléré/Luzillé rule, removing a theoretical normalisation divergence in a money-affecting rule. - The order-status email drops the function_exists guard around the pickup address, restoring the original fail-loud behaviour. - Tests: payment_reminder / processing / out_for_delivery email branches; a second Easter year (2027) asserting Ascension and Pentecost Monday; a negative add-business-days case; a positive control for the domain guard (minimum files scanned + the pattern matches known WordPress tokens); and the required-field error when the email is empty. No behaviour change. Gates green: CS-Fixer, PHPStan (max), PHPUnit (337), e2e (delivery-zone, orders, vente-loyalty). --------- Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…yer layout (#44) Mirrors the Shop reshape for the three remaining contexts so every context follows ADR 0001 (Domain/Application/Infrastructure only): - ports move from Application/Port to Domain/Gateway (and Domain/Repository for OrderTracking's TrackingAccessRepository); - OrderTracking's UserInterface/Web controller and the Loyalty / OrderTracking Bootstrap ServiceProviders move into Infrastructure; - functions.php boots the relocated providers. Pure structural move, no behaviour change. The valuable Shared dedup (Clock, NormalizedPhone) was already done in the base PR; the typed-id / Criteria / event retrofit stays intentionally deferred as disproportionate. Gates green: CS-Fixer, PHPStan (max), PHPUnit (337), e2e (tracking, loyalty, subscribers, subscription-write). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
#45) Updates present-tense references to the current code layout after the Pilotage -> Shop rename and the three-layer reshape (issue #1): - src/Pilotage -> src/Shop, tests/Pilotage -> tests/Shop - PilotageServiceProvider -> ShopServiceProvider - "admin controllers in UserInterface / assembly in Bootstrap" -> Infrastructure Across AGENTS.md, docs/fidelite.md, docs/processus-metier-commandes.md, docs/tests-vente.md and docs/spec-tableau-pilotage.md. The dated deployment-log entries in docs/prod-o2switch.md are left untouched as a historical record. Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…helper (#46) First lot of issue #3 (test-securing the dashboard). The CSV formula-injection guard was duplicated verbatim in ReceiptsController and ActivityController (`csvCell()`) and had no test. It moves into Shared\Domain\CsvFormulaGuard and gains unit coverage. Behaviour-preserving: same rule (a cell starting with = + - @ is prefixed with a quote to force it to text). Hardening the guard further (leading tab/CR, etc.) would be a separate, explicit decision. Gates green: CS-Fixer, PHPStan (max), PHPUnit (345, +8 new). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…bility (#47) Second lot of issue #3 (test-secure the dashboard). Adds a systematic security guard: it enumerates every admin_post_/wp_ajax_ action registered by the pilotage admin controllers and asserts each handler verifies a nonce (check_admin_referer / check_ajax_referer) AND a capability (assertPermission / current_user_can). Reflection-based, no WordPress boot; a positive control (minimum number of actions scanned) prevents a vacuous pass. A new write action added without a guard now fails the suite. Gates green: CS-Fixer, PHPStan (max), PHPUnit (346, +1 covering all current admin write actions). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…keys, restore) (#48) Third lot of issue #3 (test-secure the dashboard). Adds an integration e2e — `make e2e-pilotage-schema-local`, auto-discovered by scripts/e2e-ci.sh so it also runs in CI — that verifies, against the real PilotageSchemaManager: - migration idempotence (re-run without SQL error, all six tables present); - the sequence_number nullability fix (the documented dbDelta pitfall); - that the unique constraints (receipts.sequence_number, harvest_lots.code, stock_movements.reference_key) reject duplicates (a concurrency proxy); - a backup/restore round-trip. Every check runs on `CREATE TEMPORARY TABLE … LIKE` copies, so no real data is touched, and the temp tables are dropped at the end. AGENTS documents it next to the dbDelta pitfall. Gates green: CS-Fixer, PHPStan (max), PHPUnit (346), new e2e (16 assertions). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…#49) Fourth lot of issue #3 (test-secure the dashboard). Adds a deterministic performance e2e — `make e2e-pilotage-perf-local`, auto-discovered by scripts/e2e-ci.sh so it runs in CI — that fills a CREATE TEMPORARY TABLE … LIKE copy of the receipts register with 20,000 rows spread over the year, then replays the real register query on a selective window and asserts: - correctness at scale (exactly the window's rows, correct sum); - the query plan uses the occurred_at index (EXPLAIN type != ALL, key = occurred_at) — a deterministic proof it scales, not a flaky wall-clock; - a generous time guard (< 1s on 20k rows) against a catastrophic regression. Temporary table only, no real data touched. Gates green: CS-Fixer, PHPStan (max), PHPUnit (346), new e2e (5 assertions). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
…ashboard (#50) Fifth and final lot of issue #3 (test-secure the dashboard). Adds a headless-browser smoke — `make browser-local`, and a step in the CI e2e job (Node 22 + `npx playwright install`) — that logs into wp-admin and opens every pilotage view, asserting each renders without a PHP fatal or an uncaught JS error and that the tab nav and the Vente form are present. Deeper JS behaviour (the Vente anti-double-click) stays covered by the jsdom unit tests in tests-js/. Playwright is added as a theme devDependency. The browser test files (tests-browser/) and playwright.config.js are excluded from deployment in all three lists (Makefile DEPLOY_EXCLUDES, deploy-files.sh, verify-prod-integrity.sh), and the run artifacts (test-results/, playwright-report/, tests-browser/.auth/) are gitignored. Validated locally: 11 browser checks green. jsdom and PHP suites unaffected (no PHP changed). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Adds the missing entries so AGENTS covers all five #3 lots: the CSV formula-injection guard test, the admin nonce/capability reflection guard, and the receipt-register performance e2e — alongside the schema e2e and the Playwright navigation smoke already documented. Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
#52) Shared\Domain\CsvFormulaGuard now also neutralises cells that begin with a tab, carriage return or newline (OWASP CSV-injection guidance), on top of the existing = + - @ formula leads. A spreadsheet can otherwise glue such a cell to its neighbour and reinterpret it. Both dashboard CSV exports (Recettes, Journal d'activité) benefit automatically. Behaviour change limited to those leading-whitespace inputs, which are now prefixed with a quote to force plain text. Gates green: CS-Fixer, PHPStan (max), PHPUnit. Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Fixes a local dev environment whose WordPress roles drifted — a copy-of-prod database where the `administrator` role lost its capabilities, so every /wp-admin/ page returns 403 (which breaks `make browser-local`). `make doctor` (tools/local-doctor.php) recreates the standard roles, re-applies the WooCommerce caps and restores `admin` to administrator. Local-only: the tools/ folder is never deployed and the tool no-ops outside WP-CLI. Run via `wp eval "require …"` (not `eval-file`), because `declare(strict_types=1)` is illegal inside the eval() that `eval-file` uses. AGENTS documents it next to the browser smoke. Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Augments `make e2e-pilotage-perf-local`: beyond the 20k-row scale + index-usage check on a temporary copy, it now also runs the real WordPressReceiptRepository::occurredBetween() (the query AND the row -> ReceiptEntry mapping) on a small tagged dataset inserted in the real table under year 2099 (no real data), asserts the 5 entries are read and mapped correctly, and deletes the test rows in the finally block (robust even after a mid-run failure). Gates green: CS-Fixer, PHPStan (max), and the e2e (6 assertions, 0 residual 2099 rows). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Aligns the class name with its bounded context: it manages the Shop context's tables, and the "Pilotage" prefix predated the Pilotage -> Shop context rename. Pure rename, no behaviour change. The stored option key `luziapi_pilotage_receipts_schema_version` is deliberately left unchanged (it is data, not code — renaming it would trigger a one-time re-migration in production). The dashboard-feature names (PilotageController, PilotageTabs, the admin/pilotage/ views, admin-pilotage.js) keep "Pilotage" on purpose: there it is the UI name of the dashboard (« tableau de pilotage »), not the bounded context — renaming them to "Shop" would be less clear. Gates green: CS-Fixer, PHPStan (max), PHPUnit (348), e2e (schema, perf, receipt). Co-authored-by: Anthony Graule <luziapi37150@gmail.com>
Hexagonal/DDD refactor (behaviour-preserving), dashboard test suite (schema, permissions/nonces, performance, browser smoke), make doctor, and the CSV formula-guard hardening. Bumps LUZIAPI_VERSION and the theme Version header; composes the 1.4.0 CHANGELOG section.
added 4 commits
September 22, 2026 12:11
…g rename `scripts/deploy-files.sh` uploads new/renamed paths but does not remove the old ones, so a large rename (Pilotage -> Shop in 1.4.0) leaves the old src/ paths as orphans on prod. `scripts/prune-prod-orphans.sh` (and `make deploy-prune-prod` / `deploy-prune-prod-apply`) lists the src/ files present on prod but no longer tracked in git and removes them. Safe by design: same deploy gates, strictly scoped to src/, the prod file list is read via a token-protected PHP script, each orphan path is re-validated (charset, no "..") and deletion is dry-run by default (--apply + confirmation to actually remove). Documented in AGENTS.
…esidue The e2e loyalty suites reuse fixed test phones (0600000000/0600000001). Loyalty aggregates by identity (email OR phone), and autoLink() ties a test phone to the first run's email for good, so expand() re-aggregates residual ledger data left by a run interrupted before its cleanup — inflating the baseline of later runs. The tool purges only the identity cluster reachable from those fake test numbers (identity links + residual ledger), dry-run by default, with a size guard and a token-protected prod runner.
…ndler ctor) The production-only loyalty e2e core still built GetCustomerLoyaltyHandler with a WordPressClock as its second argument. That parameter became ?LoyaltyIdentityLinks in 1.3.0 (identity linking, commit 74c4ea3), so the construction threw an uncaught TypeError before the try block — an empty HTTP response (no JSON). The core is prod-only (never run locally nor in CI), so the rot went unnoticed until the full prod e2e sweep. Production wiring (LoyaltyServiceProvider) was always correct; this is purely a stale test. Also drop the misleading '(expiration active)' assertion label — pot expiry was removed in 1.3.0.
Record the 1.4.0 release deploy (245/245 SHA, base cded01e -> 9b2d980), the Pilotage -> Shop orphan pruning (190 files removed, verify-prod 350/350), and the full prod e2e sweep (13/13) with the two test-only fixes it surfaced: the loyalty test-isolation residue purge and the stale prod-only loyalty e2e core.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 1.4.0. Behaviour-preserving — no functional change for shoppers or the shop owner.
What's in it
src/(Shop core, Loyalty, Newsletter, OrderTracking) + a Shared kernel, three layers, pureinc/business rules extracted into WordPress-free, unit-tested domain classes. An architecture test forbids WP/WC in anyDomain/.make doctorfor local role/cap repair.The diff since 1.3.1 is 347 files (186 renames, 5 deletions) but no new Composer runtime dependency (
vendor/unchanged).Do not use the usual targeted
scripts/deploy-files.shdelta: it does not remove the old renamed paths (src/Pilotage/*would linger as orphans) and pushing ~280 files over FTPS risks an interrupted upload → a missingsrc/Shopclass → fatal on every page (the 10 Sept incident).Recommended: a full
src/mirror with--delete(removes the oldsrc/Pilotage) + the changedinc//functions.php, thenmake verify-prod(SHA of every tracked theme file), OPcache reset, an uncached-URL smoke (/wp-login.php,/mon-compte/), and themake e2e-*-prodvariants (isolated, no e-mail) as the final proof.Do not merge until after a successful deployment (GitFlow: deploy from this open branch, then merge + tag on an explicit go-ahead).