Skip to content

<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /changelog/Changelog.4.111.md for this page in Markdown format</div>

Home | Changelog

Version 4

version 4.111

  • [4.111.3] fix(build): resolve eagerly-loaded jsCapabilities chunks to their content-hashed files (Advisable-com/ecommercen#379 follow-up)

    • The regression. #379 added output.chunkFilename: '[name].[contenthash].js', which made front-mix-manifest.json keyed by the hashed chunk name. But application/config/main.php's jsCapabilities block — rendered eagerly as <script> via assetUrl($src, '000') in renderJsCapabilities(), e.g. pushJsCapability('checkout-page') in the checkout view — looks chunks up by their bare path (/ui/main/js/checkout-page.js). assetUrl()'s exact-key manifest lookup then missed and fell back to …/checkout-page.js?v=000: a frozen cache-buster on a bare file the build no longer regenerates. The eager checkout-page (and 6 other capability) scripts thus regressed from properly busted (?id=&lt;contenthash>) to permanently stale.
    • The fix. A mix.then() post-build step in webpack.mix.front.js augments front-mix-manifest.json with a stable bare-name alias for every content-hashed chunk (/ui/main/js/checkout-page.js/ui/main/js/checkout-page.&lt;hash>.js). assetUrl() now resolves the eager (jsCapabilities) path to the same hashed file the webpack runtime loads lazily — both paths are content-addressed and consistent. Mirrors the bare→hashed mapping the planned Vite migration provides natively.
    • No DB / REST / language changes. Admin is unaffected (no jsCapabilities).
    • Verify on merge: after a production build, front-mix-manifest.json contains "/ui/main/js/checkout-page.js": "…/checkout-page.&lt;hash>.js…"; loading checkout issues a single request to the hashed checkout-page.&lt;hash>.js (no ?v=000 bare request, no 404).
  • [4.111.3] fix(build): content-hash webpack code-split chunks to stop stale-asset checkout outages (Advisable-com/ecommercen#379)

    • The bug. laravel-mix .version() versions only manifest entries (the ?id= query strings resolved by assetUrl()), not the runtime code-split chunks loaded by the webpack runtime (public/ui/main/js/manifest.js). Those chunks (checkout-page, etc.) kept bare, unhashed filenames across releases, so when a chunk's content changed its URL did not — CDNs/browsers served the stale chunk, breaking checkout across clients (both pipeline- and dev-built). Upstream: laravel-mix #3186.
    • The fix. webpack.mix.front.js / webpack.mix.admin.js add mix.webpackConfig({ output.chunkFilename: '&lt;dir>/[name].[contenthash].js', optimization.{moduleIds,chunkIds}: 'deterministic' }) (front ui/main/js/, admin ui/admin/dist/), so dynamically-imported chunks are content-addressed — a content change yields a new URL. Deterministic ids keep chunk identity stable so the hash tracks real content, not id churn. Entry bundles and .combine()/.version()/mix-manifest behavior are unchanged. (The eager jsCapabilities load path is handled by the follow-up entry above.)
    • Mid-deploy hardening. assets/main/vue/vueapp.js wraps the lazy async-component import factories in a lazyChunk() helper that, on a ChunkLoadError, performs a single sessionStorage-guarded location.reload() to fetch the fresh manifest + chunks (one reload per page-life and per session; no loop if the chunk is genuinely gone; re-arms on a successful load).
    • No DB migration, REST API change, or language-key changes. Stopgap for the broader Vite migration (content-hashes chunks by default), tracked separately.
  • [4.111.3] fix(blog): set isBlog on author pages so they get the blog storefront treatment (Advisable-com/ecommercen#385)

    • The bug. In ecommercen/blog/controllers/Adv_blog.php, blogAuthor() was the only blog action that omitted 'isBlog' => true from its render array — its siblings index(), blogArticle(), blogCategory(), blogTag(), and searchPage() all set it. The omission caused blog author pages to miss the isBlog-gated storefront treatment: blog LD+JSON structured data (application/views/production/ld_json.php) and blog-search/priority-nav JS under solrSearch v1 (application/views/main/components/footer/footer_js.php).
    • The fix. Added 'isBlog' => true to blogAuthor()'s render array, consistent with the sibling actions. One-line change; no DB migration, REST API change, or language-key changes.
  • [4.111.3] fix(category): pass $record to getMuiEditPost so non-ADVISABLE edits preserve the category slug (Advisable-com/ecommercen#386)

    • The bug. In ecommercen/category/controllers/Adv_category_admin.php, getMuiEditPost() referenced $record which was only a local variable in edit() and was never passed to the hook. For ADMIN/CMS admins the slug-preservation guard $hasSlug therefore always evaluated false, causing the slug to be silently regenerated via create_slug() on every save — overwriting manually-customized slugs and changing the category URL whenever the name was edited. ADVISABLE-role admins were unaffected (they derive $hasSlug from the posted slug field).
    • The fix. getMuiEditPost() now accepts an optional $record = null parameter (backward-compatible). edit() passes the already-fetched $record, whose MUI rows are attached as $record->{lang} by getAdminRecord(), so $record->{$langAbbr}->slug correctly detects an existing slug and preserves it. No other callers or client overrides of this method exist.
    • No DB migration, REST API change, or language-key changes.
  • [4.111.3] feat(eshop): make category Agora sponsored slot positions config-driven (Advisable-com/ecommercen#380)

    • What & why. The category listing's Agora sponsored-product slot positions were hardcoded inline in Adv_product_categories::projectAgoraAll() ($isCat0 ? [1, 2, 3, 4] : [1, 2, 3, 9, 13, 15, 21]), so a shop wanting a different slot layout had to full-copy the ~106-line method into an override. That is exactly the merge-drift that silently orphaned a client override when 4.x consolidated the old per-feature projectAgora* methods into the async projectAgoraAll(). Realizes those methods' own // @TODO move to config.
    • The change. projectAgoraAll() now reads the slot list from two config keys with inline fallbacks to the previous defaults — agoraSponsoredPositions (non-cat0 pages) and agoraSponsoredPositionsCat0 (cat0 pages) — registered in application/config/app.php with the historical arrays. A shop overrides just the config value (in its own config) — no code override. Default output is byte-identical for every existing shop.
    • Housekeeping. Removed the now-orphaned legacy methods superseded by projectAgoraAll() that still carried the duplicate hardcoded arrays / per-feature flows: projectAgoraItems(), projectAgoraItemsCat0(), projectAgoraBanners(), projectAgoraDsplItems(), and projectAgoraDsplItemsV2() (its dsplFirst+dsplSecond merge is now done inline by projectAgoraAll()). Verified zero callers across the repo (the same method names in Adv_search/Adv_vendors/Adv_order are those controllers' own copies and are untouched). Also dropped a stale leftover docblock above projectAgoraAll().
    • No DB / route / DI / API-contract / frontend / language changes.
    • Verify on merge: category pages render sponsored products in the same slots as before with no config override; setting $config['agoraSponsoredPositions'] = [1, 2, 3, 9, 13, 15, 21, 22, 23, 24] lands the 8th–10th sponsored products in slots 22/23/24 instead of being appended at the end.
  • [4.111.3] refactor(apifon): resolve Apifon through the DI container

    • What changed. Advisable\Apifon\Apifon is now a Symfony DI service registered in src/Apifon/container.php (wired into application/config/container/modules.php) and resolved via di()->get(Apifon::class) at all call sites, replacing the previous manual new Apifon(...) + CI-registry construction. The named 'apifon' logger (NamedLoggerInterface) is now constructor-injected into the service; the old in-body di() logger fallback is gone.
    • Call sites updated. ecommercen/helpers/sms_helper.php (2 call sites) and ecommercen/eshop/controllers/Adv_order.php (1 call site) now obtain the instance through di()->get(Apifon::class).
    • \Registry stays out of the container — by design. CI's library loader (system/core/Loader.php _ci_init_library) resolves libraries through the container by bare class name: if di()->has($className) it calls di()->get($className) instead of new $className(). Because the registry library's class name is exactly Registry, adding a service under that id hijacks CI's own library load and recurses at boot (500 on every page). A RegistryFactory approach was attempted and reverted. \Registry is therefore resolved lazily at request time via a private registry() helper on the service (same pattern as Domains\Features\FeatureRegistry), not injected as a constructor dependency. A NOTE documenting this constraint is now in application/config/container/container.php.
    • No externally visible changes — no REST API version bump, no DB migration, no language-key changes.
    • Follow-up. The same lazy-DI pattern should be applied to the remaining manually-new'd integrations: Moosend (#381), Manago (#382), BunnyStream (#383), ShopflixOrders (#384).
  • [4.111.1] fix(rest): localize REST payment-gateway order descriptors to match the legacy storefront

    • The divergence. Placing the same order via the storefront vs. the REST API produced different descriptions on the acquirer statement. The legacy Adv_checkout controller renders localized descriptors via t() keys; the REST payment adapters hardcoded fixed-English strings ("Order {serial} - {site}", "Order #{serial}", etc.). Surfaced by a client (Seajets) reconciling Nexi statements. Cosmetic only — order matching keys on order_serial/tran_ticket/securityToken, not the descriptor — but inconsistent across checkout paths.
    • The fix. Each adapter whose legacy counterpart uses a localized t() descriptor now calls the same key with the same argument order, so the descriptor is byte-for-byte identical and localized for both paths:
      • XPayAdaptercheckout.xpay.order_description (Nexi description).
      • CardLinkAdapter alphacheckout.alpha.order_description (via a new optional orderDescriptionKey); eurobank keeps the legacy "{site} Order {serial}" literal it already matched.
      • EthnikiAdapter / EthnikiEEAdaptercheckout.ethniki.order_description (Simplify data-name / NBG order.description).
      • VivaWalletAdaptert('hct_eshop_name') . ' ' . serial (customerTrns).
      • PayPalExpressAdaptert('hct_paypal_express_description') (description; serial still carried in reference_id).
    • All keys already exist in the 8 locale files — no language changes. Adapter orderDescriptionPrefix constructor params renamed to siteName for consistency with XPayAdapter; the factory passes config_item('site_name') to each.
    • Deliberately unchanged (no localized legacy descriptor to match): PayPalAdapter (advanced — order built client-side via the JS SDK), StripeAdapter and KlarnaAdapter (real per-product line items; the unused checkout.klarna.order_description key is left as-is), and EthnikiNbgPayAdapter (sends no gateway descriptor).
    • Tests. Updated the adapter tests to assert the localized descriptors; added VivaWalletAdapterTest and a CardLink alpha-path case. Added a namespace-scoped t() shim (tests/Support/t_helper_shim.php, mirroring the existing config_item locale shim) so the pure-Unit suite resolves the adapters' t() calls deterministically and the Integration suites delegate to the real CI helper.
    • Verify on merge:
      • vendor/bin/phpunit tests/Unit/Checkout/Payment/Adapters/ — all adapter descriptor tests green (incl. the new VivaWalletAdapterTest and the CardLink alpha-path case).
      • Place a test order through XPay / Alpha / Ethniki / EthnikiEE / Viva / PayPal-Express via both the REST API and the storefront, and confirm the descriptor matches across paths — e.g. XPay → xpay_logging.request_data order.description reads "Transaction {serial} from {site}"; CardLink alpha → gateway orderDesc; Viva → customerTrns reads "{eshop_name} {serial}". In a Greek-locale deployment the descriptor should render in Greek, not English.
      • Confirm the deliberately unchanged adapters still behave: Eurobank sends the "{site} Order {serial}" literal; PayPal-advanced / Stripe / Klarna descriptors are untouched.
      • No DB migration and no new lang keys — every checkout.*.order_description and hct_* key already ships in all 8 ecommercen/language/*/ files.
  • [4.111.0] feat(rest): GET /rest/storefront-config — governed structured-settings resource (Advisable-com/ecommercen#374)

    • What it is. A curated, typed, sectioned REST resource exposing backend-owned structured storefront settings to headless clients — the non-boolean sibling of /rest/features. Where /rest/features is a flat boolean capability map (with FeatureGuardMiddleware enforcement), this carries parameter values (numbers, lists, strings) grouped into named sections, with no server-side gating. Guest auth, read-only, global (not per-user/locale). REST API bumped to v1.12.
    • First section listing. { perPageDefault, perPageOptions[], perPageMax } derived live from the storefront config (products_list_limit / pageLimitsArray() / products_list_max) so a headless client renders its per-page selector identically to the rendered storefront and tracks whatever the deployment configures — no rebuild. Default deployment returns { perPageDefault: 36, perPageOptions: [9, 18, 36, 72], perPageMax: 72 }.
    • Governed like features, not a registry dump. Sections/keys are curated and typed in code (Advisable\Domains\StorefrontConfig\StorefrontConfigProvider) and mirrored by the OpenAPI schema; each new section/key is a deliberate, reviewed contract addition (same discipline as adding a feature key). It is not an arbitrary config/registry passthrough — exposed values get a review gate and the frontend stays decoupled from internal config names. Platform-owned shape, destined for velora-contracts-base.
    • Why a sectioned resource (not a per-setting endpoint). Many backend-owned settings will need exposing over time (per-page, default locale/currency, free-shipping threshold, loyalty point factors, …). A new typed section slots in with no new endpoint each time. Kept separate from /rest/features (different semantics — values vs gates, no middleware enforcement).
    • Consumer. velora#269 — wired via a general useStorefrontConfig() with an app.config.ts fallback (catalog.perPage = default 36 / options [9,18,36,72] / max 72); picks up this resource automatically once available (backend-over-fallback, same as features). No velora redeploy needed when it lands.
  • [4.111.0] fix(test): stop ThemeHelperProductImagesJsonTest config stub self-recursing on non-base_url keys (Advisable-com/ecommercen#375)

    • The bug. The test's anonymous config stub delegated every key except base_url back through get_instance()->config->item($key). But the same setUp() registers that stub as get_instance() (GetInstanceRegistry::set($this->ciStub)), so the delegation re-entered the stub and recursed until Xdebug aborted at 512 frames — erroring all 21 tests. Upstream stayed green only because its assetUrl() reads base_url alone; a client whose MY_url_helper::assetUrl() override reads another key (e.g. config_item('app_version') as an asset cache-buster, as in the wecare fork) tripped it, masking the #325 JSON-shape and #372 resize-key coverage on those forks.
    • The fix. Capture the real CI config object before the get_instance() swap and delegate non-base_url keys to it, preserving the original intent. base_url stays stubbed for predictable assertions; every other key resolves from the real bootstrapped config regardless of which keys a client's assetUrl() reads. Added a regression test that reads a non-base_url key through the swapped instance to pin the no-recursion contract (verified red against the old delegation, green against the fix). Test-harness only — no runtime code changed.