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.101.md for this page in Markdown format</div>

Home | Changelog

Version 4

version 4.101

  • [4.101.2] fix(meta-capi): silence FacebookAds CrashReporter stdout writes that flooded PHP-FPM stderr (Advisable-com/ecommercen#232)

    • Symptom. wecare PHP-FPM containers shipped ~4GB/day of FacebookAds\CrashReporter : Enabled lines to Loki/S3 — one per HTTP request — crowding out real signal in slow-log analysis. Discovered while building a PHP-FPM slow-log → JSON pipeline cluster-side.
    • Root cause. Advisable\MetaConversionsApi\Service\FacebookConversionService::__construct (line 80) called Api::init($pixelId, null, $pixelAccessToken) without the 4th $log_crash argument, which defaults to true in the FB Business SDK. That triggers FacebookAds\CrashReporter::enable(), which fwrite()s FacebookAds\CrashReporter : Enabled direct to php://stdout per request — bypassing PHP's error_log machinery entirely, so the ini directive cannot silence it. The CrashReporter also attempts to upload crash data to Facebook on shutdown.
    • Fix. One-line change in src/MetaConversionsApi/Service/FacebookConversionService.php:80 — passes false as the 4th arg: Api::init($pixelId, null, $pixelAccessToken, false). Disables the SDK CrashReporter at init. No SDK upgrade required; the parameter has been part of Api::init for many SDK versions. Takes effect on the next deploy of any tenant using Meta CAPI.
    • Scope check. Only one production call site of Api::init exists in the codebase (grep -rn 'Api::init('). Worth checking client repos (seajets etc.) on next upstream sync — same default would apply anywhere a tenant overrides FacebookConversionService or constructs the SDK directly.
    • Docs. docs/flows/integration/IN-07-facebook-catalog-capi.md updated to document the explicit $log_crash=false argument.
  • [4.101.2] fix(doc-ba): guardrail against orchestrator-supplied template lines that break VitePress (Advisable-com/ecommercen#246)

    • Root cause. The SY-33 breadcrumb that broke the 4.101.0 docs deploy (resolved by hotfix 4.101.1) did not originate in .claude/agents/doc-ba-document.md or .claude/skills/doc-ba-workflow/SKILL.md — both templates correctly start at # {Title}. It came from the orchestrator's prompt when dispatching the agent for SY-33; the agent dutifully followed the malformed template it was given. No guardrail at the agent level prevented a bad template from reaching disk.
    • Fix. Strengthens the existing "Match the existing doc style" instruction (#5) in .claude/agents/doc-ba-document.md. The agent now must Read the first ~20 lines of a sibling doc in the same category before writing a new flow doc and conform to the corpus head byte-for-byte. If the orchestrator-supplied template includes a line no sibling carries (breadcrumb, alternate frontmatter, speculative nav header), the agent drops it and follows the corpus. The instruction also explicitly notes that VitePress production builds run in strict-link mode and that dead relative links ((./index), (./index.md), etc.) abort the Cloudflare Pages deploy without affecting the Docker image step.
    • Reference docs cited as canonical examples: docs/flows/admin/AD-13-settings.md, docs/flows/admin/AD-03-order-management-admin.md, docs/flows/system/SY-32-logging.md — all start at # Title with no breadcrumb.
    • Closes Advisable-com/ecommercen#246.
  • [4.101.1] fix(docs): drop dead breadcrumb in SY-33 flow doc that broke the VitePress production build

    • The breadcrumb header [Home](../../Home.md) | [Flows](../index.md) | [System](index.md) at the top of docs/flows/system/SY-33-bot-detection-session-exclusion.md referenced a non-existent docs/flows/system/index.md. VitePress catches dead links during production build and aborted with [vitepress] 1 dead link(s) found. The Bitbucket Pipelines *.*.* tag job built and pushed the Docker image successfully but the subsequent docs-deploy-to-Cloudflare-Pages step failed and the 4.101.0 docs never reached upstream-docs-ecommercen.pages.dev.
    • Removed the breadcrumb line entirely — none of the other system flow docs (SY-01 cron-framework, SY-31 rest-api-versioning, SY-32 logging) carry a breadcrumb. SY-33 now matches the convention: file starts at the # SY-33: … H1.
    • No content change. Bumps application/config/version.php to 04.101.001.000 so the tag triggers a fresh pipeline run that completes both Docker image push and docs deploy.
  • [4.101.0] feat(session): use matomo/device-detector for bot detection in MY_Session to stop creating sessions for modern AI crawlers

    • Root cause. MY_Session::isBotSession() (application/libraries/Session/MY_Session.php:21) delegated to CodeIgniter's User_agent::is_robot(), which matches against the hand-maintained list of ~88 substrings in application/config/user_agents.php. The list is years out of date and misses every modern AI crawler — GPTBot, ClaudeBot, OAI-SearchBot, PerplexityBot, CCBot, Amazonbot, Applebot, Bytespider, etc. — and many security scanners and uptime checkers. Those crawlers were getting full CI sessions: rows in the session store, Set-Cookie headers, session regenerations, all of it.
    • Fix in application/libraries/Session/MY_Session.php:21-39. Replaced the body of isBotSession() with a DeviceDetector\DeviceDetector parse against $_SERVER['HTTP_USER_AGENT']. Empty UA short-circuits to true (no real browser omits it). discardBotInformation() is set before parse() so the detector runs the fast bot-only path (no OS/client/device parsing). On any DeviceDetector failure the method fails open (returns false) and logs via log_message('error', ...) (PSR-3 → Monolog) so a corrupted YAML or OOM never blocks real users from logging in.
    • What stays the same. isRouteExcluded(), the session_excludes.php config, the constructor early-return ordering — all untouched. CodeIgniter's User_agent library and application/config/user_agents.php stay in place because other call sites (agent->referrer(), agent->getOwnRedirectUrl() in Adv_customer.php and ChannelTrack.php) still depend on them.
    • Smoke verified against develop.local. GPTBot, ClaudeBot, Googlebot, and empty-UA requests now return no Set-Cookie header; real Chrome and garbage-UA strings (asdf) get sessions as before.
    • Follow-ups (not in this PR). Three other is_robot() consumers can migrate to the same detection in separate PRs: ecommercen/core/Adv_front_controller.php:321 (shouldLoadAdvisableAI() — highest financial upside; AI calls currently fire on every GPTBot page hit), ecommercen/api/controllers/AdvApiCartController.php:269 (destroy() cart guard), and the doc references in docs/changelog/Changelog.4.30.md:40 + docs/flows/customer/CF-34-ai-recommendations.md.
  • [4.101.0] feat(rest): port Product/PriceTracking write layer + graph service + job to modern domain layer (Advisable-com/ecommercen#128)

    • Write layer. New Advisable\Domains\Product\PriceTracking\{WriteData,Validator,WriteService,Repository\WriteRepository}. The WriteService is registered in DI and used internally by the modern TrackPrices job. POST/PUT/DELETE REST routes remain commented in application/config/rest_routes.php — write access stays system-only, exactly as before.
    • Graph service. Ported AdvPriceTrackingGraphsAdvisable\Domains\Product\PriceTracking\GraphService. The Omnibus reference-price logic (hasPriceChanges, isSuccessivePriceReductions, getHigherPriceOfPeriod, getLowestPriceOfPeriod, getNextLowestPriceOfPeriod, hasLowerPriceThanFinalPrice) is preserved byte-for-byte to keep storefront output identical.
    • Options DTO. Ported AdvPriceTrackingOptionsAdvisable\Domains\Product\PriceTracking\Options (immutable readonly, public properties preserved for view-template parity). Constructed via Options::fromRegistry() factory and registered as a DI service so views/helpers/controllers resolve a single shared instance via di()->get(Options::class).
    • PriceCalculator. Extracted the final-price math (regular + special discount + VAT) into Advisable\Domains\Product\PriceTracking\PriceCalculator as a framework-free, unit-testable service. The CI helpers applyVatWithoutFormat / formatNumber were inlined as round(...) + sprintf(...) so the calculator no longer depends on shopmodule_helper.
    • Job port. AdvTrackPrices is now Advisable\Domains\Product\PriceTracking\Jobs\TrackPrices, implementing \JobCommand directly. It injects the modern WriteService and PriceCalculator. application/config/jobs.php now references the FQCN (matches the RemoveExpiredRefreshTokens pattern).
    • New REST graph endpoints. GET /rest/product/price-tracking/graph/{productId} and GET /rest/product/price-tracking/graph?productIds=1,2,3 (replacing the legacy /api/priceTracking/... controller). Annotated with OpenAPI; bulk endpoint takes a comma-separated productIds query param.
    • Legacy files removed (10 files). ecommercen/job/libraries/AdvTrackPrices.php, ecommercen/eshop/libraries/AdvPriceTrackingGraphs.php, ecommercen/eshop/libraries/AdvPriceTrackingOptions.php, ecommercen/eshop/models/AdvPriceTrackingModel.php, ecommercen/api/controllers/AdvApiPriceTrackingController.php, plus the 4 application/modules/... client-overridable subclasses (TrackPrices, PriceTrackingOptions, Price_tracking_graphs, Price_tracking_model, Api_price_tracking) and the legacy unit test tests/Legacy/Eshop/AdvPriceTrackingGraphsTest.php.
    • Call sites rewired (8). ecommercen/core/Adv_front_controller.php, ecommercen/helpers/theme_helper.php, ecommercen/eshop/models/Adv_product_parser_model.php (3 sites), and 5 views (application/views/main/components/footer/footer_json_adv_app_context.php, application/views/admin/footer_json_adv_app_context_admin.php, application/views/admin/products/list.php, application/views/admin/settings/settings.php, application/views/main/components/live_search/live_search.php, live_search_v2.php) now resolve the Options DTO and GraphService via di()->get(...).
    • Legacy /api/priceTracking/... routes removed. application/config/routes.php cleaned up; the 2 dispatcher entries are gone. Existing storefront/frontend callers should be repointed at the new /rest/product/price-tracking/graph[/{productId}] endpoints.
    • Tests. 4 new unit-test classes (tests/Unit/Domains/Product/PriceTracking/{GraphServiceTest,PriceCalculatorTest,ValidatorTest,WriteDataTest}.php, 38 tests, 41 assertions). Replaces the deleted AdvPriceTrackingGraphsTest. Post-merge with develop (which brought #11, #78, #230, plus the Database-suite-green fixes), the full grand-total is 5,866 tests passing across Unit (2,663) + Integration (89, 37 self-skipped on Redis/APCu) + Legacy (207, 6 self-skipped on POSIX-only branches) + Database (2,907) with 0 failures and 0 errors.
    • DI registration. src/Domains/Product/container.php registers the 6 new services; src/Rest/Product/container.php adds the $graphService arg on the controller.
  • [4.101.0] feat(rest/transporter): live smart-point catalog endpoint for guest checkout (Advisable-com/ecommercen#237)

    • Endpoint. New GET /rest/transporter/{transporterId}/smart-point — guest-accessible, returns the live pickup-point catalog for a single transporter. Used by Velora's useSmartPoints composable for the checkout pickup picker.
    • Service. New Advisable\Domains\Transporter\SmartPoint\SmartPointCatalogService aggregates an existing src/SmartPoints/Transporters/* provider per request. No caching — mirrors the legacy Adv_order::smartPointsInitialize behaviour. Org-wide REST caching policy is under separate discussion in Advisable-com/ecommercen#241; this endpoint is the first adopter candidate once a policy lands.
    • Controller. Thin Advisable\Rest\Transporter\Controllers\SmartPoint maps typed service exceptions to envelope codes: 200 on success, 404 on unknown transporter, 409 on disabled provider, 502 on upstream failure.
    • Auth. auth=guest — matches legacy Adv_order (Front_c) exposure. The pre-existing /rest/order/smart-point (per-order saved row, backend-only) is unchanged; the two endpoints are distinct concepts and stay distinct.
  • [4.101.0] fix(tests): Database suite green end-to-end — DI bootstrap + transaction-isolation + seed-id partitioning

    • Container::markBootReady() added to tests/bootstrap_ci.php. Without it, Container::getContainer() returns the stub container in every CI-backed test because the pre_controller hook never fires under the test bootstrap. Every di()->get(...) from inside a test's setUp() or production code-under-test was throwing ServiceNotFoundException — accounting for ~2900 of the ~2904 baseline errors in the Database suite. LoggerDiBindingTest's manual reflection-based bootReady toggle (added 4.99.15) is now redundant but harmless.
    • CiDatabaseTestCase transaction wrapper rewritten. The old version used $db->trans_begin() / trans_rollback(), but CI3's reference-counted trans system short-circuits at depth > 0 without issuing a real BEGIN. When BaseWriteRepository::transactional() inside the SUT called trans_start(), it would either no-op (if depth > 0) or — worse — fire its own START TRANSACTION when depth was 0, which MySQL semantics treat as an implicit COMMIT of any active transaction. Inner writes leaked permanently, defeating the per-test rollback. The fix opens the test transaction with a raw START TRANSACTION query and then primes CI3's private _trans_depth = 1 via \Closure::bind() so nested trans_start() calls just increment the counter instead of starting a new MySQL transaction. tearDown() issues ROLLBACK and resets depth to 0. Documented in inline comments referencing the CI3 driver behaviour at system/database/DB_driver.php.
    • TestSeed moved from id=99001 to id=888001 across all 6 entities. The 99000-99999 range is the established convention for per-test-class fixtures (each Integration test inserts its own row at 99001 etc.). The seed overlapping at the same id caused silent PK collisions: test INSERTs against the existing seed row failed, then reads returned the seed's data, producing mysterious assertion failures. id=888001 is far above the test fixture range so no collision is possible. Tests that explicitly reference seed rows as FK targets (only Product\Product\{Repository,Service}Test's vat_id) updated to point at 888001.
    • Two stale-test fixes carried over from the #230 investigation: Product\Product\ServiceTest::createData() gained the now-required productCodes field (validator tightened by #125), and Seo\DefaultMetaTag\ServiceTest had for=3 / for=5 updated to valid 1 / 2 enum values (validator restricted by #45). These weren't tracked as bugs because the underlying tests had been silently failing inside the broader Database suite breakdown.
    • Result. Database suite went from 2904 errors + 208 failures (out of 2907 tests) to 2907 / 2907 passing. Full grand-total across Unit + Integration + Database + Legacy: 5,860 tests pass, 0 failures, 0 errors.
  • [4.101.0] refactor(repository): inject CI_DB_query_builder into BaseRepository and BaseWriteRepository via DI (closes Advisable-com/ecommercen#230)

    • Antipattern. Both Advisable\Domains\Support\Repository\BaseRepository::__construct() and BaseWriteRepository::__construct() reached into the CI3 superobject ($CI =& get_instance(); $this->db = $CI->db;) to obtain their CI_DB_query_builder. Because every concrete domain repository in src/Domains/ (and every client custom/Domains/ repo) inherits from these classes, the entire repository layer was untestable without booting the full CI stack. BaseWriteRepositoryTest papered over it with a ReflectionClass::newInstanceWithoutConstructor() hack plus reflection on a private property — comments in the test even documented why the constructor had to be bypassed.
    • Fix (Strategy A, optional + di() fallback). Both base constructors now accept ?CI_DB_query_builder $db = null and resolve $this->db = $db ?? di()->get(CI_DB_query_builder::class). Direct callers (production via Symfony autowiring of the factory registered in #78, tests via explicit mock injection) get pure constructor injection; the di()->get() fallback survives only for legacy callers that haven't been migrated to pass $db through. The get_instance() call is gone from the runtime path. Mirrors the optional-logger pattern shipped in 4.99.15.
    • Why this approach. ~80 concrete Repository.php subclasses currently override __construct(RepositoryConfigurator $configurator) { parent::__construct($configurator); } purely to narrow the configurator type for Symfony autowiring. Making $db a mandatory parent arg would require editing every one of those subclasses (plus the same in every client fork on next upstream sync) to forward $db through. Optional-with-fallback achieves the same runtime semantics in 2 files, leaves subclass signatures unchanged, and preserves backward compatibility for the dozens of direct-instantiation callers that haven't been audited yet. Strict mandatory-arg propagation is tracked separately for a quieter moment as Advisable-com/ecommercen#231.
    • Tests. New tests/Unit/Domains/Support/Repository/BaseRepositoryTest.php (3 tests) pins the constructor contract: explicit CI_DB_query_builder injection, configurator relations stored, subclass-declared $table exposed. BaseWriteRepositoryTest drops its newInstanceWithoutConstructor() reflection hack in favor of new ConcreteWriteRepository($this->db) — same 10 method tests now exercise the real constructor path. Full Unit suite: 2625/2625 (+3 vs prior baseline); LoggerDiBindingTest 5/5 confirms the container compiles cleanly under the new fallback path.
  • [4.101.0] fix(product): null shop_product.shelfcode_id references on shelf code delete (closes Advisable-com/ecommercen#11)

    • Root cause. Advisable\Domains\Product\Shelfcode\WriteService::delete() delegated straight to BaseWriteRepository::delete(), which only ran DELETE FROM shop_product_shelfcodes WHERE id = ?. The legacy Adv_shelfcodes_model::delete_record() (ecommercen/eshop/models/Adv_shelfcodes_model.php:113-118) performs a soft-dereference first — UPDATE shop_product SET shelfcode_id = NULL WHERE shelfcode_id = ? — and only then deletes the shelf-code row. The modern path skipped step one, so DELETE /rest/product/shelfcode/{id} either 500'd on the FK (where the constraint exists) or orphaned shop_product.shelfcode_id references pointing at a no-longer-existent shelf code.
    • Fix. New Shelfcode\Repository\WriteRepository::nullProductReferences(int|string $id) runs the same UPDATE shop_product SET shelfcode_id = NULL WHERE shelfcode_id = ?. WriteService::delete() now wraps the pair inside writeRepository->transactional() — null-out the references first, then delete the shelf code — matching the Cms\Builder\WriteService::delete() cascade pattern. Returns false on transaction rollback or repo failure; partial cascades can no longer leak.
    • Tests. New tests/Unit/Domains/Product/Shelfcode/WriteServiceTest.php (5 tests) asserts call ordering (nullProductReferences runs before the delete call), false return on transactional() returning null, false return on WriteRepository::delete() returning false, plus two smoke tests pinning the create()/update() happy paths so the delete rework cannot silently regress its neighbours. Full Unit suite: 2622/2622 (+5 vs prior baseline).
  • [4.101.0] refactor(checkout): inject CI_DB_query_builder into StockService (closes Advisable-com/ecommercen#78)

    • Antipattern. Advisable\Domains\Checkout\StockService::__construct() reached into the CI3 superobject ($CI =& get_instance(); $this->db = $CI->db;) instead of declaring its CI_DB_query_builder dependency. The service bypassed the DI container, hid its dependency from src/Domains/Checkout/container.php, and forced tests/Unit/Domains/Checkout/StockServiceTest.php to instantiate via ReflectionClass::newInstanceWithoutConstructor() and inject the DB mock through reflection on a private property.
    • Fix. Constructor now takes private CI_DB_query_builder $db as a typed parameter — the standard DI pattern documented in .claude/rules/domain-layer.md. The test drops the reflection hack in favor of new StockService($mock).
    • New factory-backed DI registration for CI_DB_query_builder. A new src/CodeIgniter/QueryBuilderFactory::create() returns get_instance()->db; application/config/container/container.php registers it as the factory for CI_DB_query_builder (->set(CI_DB_query_builder::class, CI_DB_query_builder::class)->factory([QueryBuilderFactory::class, 'create'])). Mirrors how AppLoggerFactory::create backs LoggerInterface in application/config/container/logger.php. Any future service that wants the query builder via DI can now type-hint it in its constructor with zero per-module wiring. Also tidied a stale unused Reference import from container.php.
    • Follow-up. Advisable-com/ecommercen#230 tracks the same migration for BaseRepository::__construct() and BaseWriteRepository::__construct(), which still use the get_instance() antipattern and are inherited by the entire domain-repository layer.
    • Tests. Unit suite 2617/2617 (matches the pre-change baseline); LoggerDiBindingTest 5/5 confirms the container compiles cleanly with the new factory registration.
  • [4.101.0] fix(gifts): use canonical filterApplicableGiftRules() in admin order gift validation (Advisable-com/ecommercen#205)

    • Root cause. Adv_orders_admin::validateProductGiftSelections() at ecommercen/eshop/controllers/Adv_orders_admin.php:2624 reimplemented the canonical Adv_gifts_model::filterApplicableGiftRules() (ecommercen/eshop/models/Adv_gifts_model.php:512-535) as an inline array_filter, but the inline copy lacked the rule 13 carve-out. Rule 13 ("Products required - get cheapest free") does not use gift_choices, so its gift_choices[1] is normally empty — the inline filter dropped it unconditionally while the canonical method called earlier at line 1513 keeps it. The admin order edit screen then diverged for Rule 13 gifts (visible in one widget, missing from validation, or vice versa). Storefront checkout was unaffected because it only ever used the canonical method.
    • Fix in ecommercen/eshop/controllers/Adv_orders_admin.php:2624. Replaced the 18-line inline array_filter closure with $this->gifts_model->filterApplicableGiftRules($availableGiftRules, $applicableGiftIds). Both call sites (1513 and 2624) now share the single Rule-13-aware canonical method, eliminating the divergence.
  • [4.101.0] fix(gifts): strip dom_gift_ID on rule 13 save and purge orphan gift_choices rows (Advisable-com/ecommercen#206)

    • Root cause. The rule 13 admin form POSTed dom_gift_ID but postDataRuleCleanupHelper() had no removeFields entry for it, so the field was written as-is to gift_choices. Rule 13 runtime (Adv_gifts_model.php:511-535, :1172-1227) never reads gift_choices — the rows were dead on arrival.
    • Fix in ecommercen/eshop/models/Adv_gift_rules_model.php:133. Appended 'dom_gift_ID' => 1 to rule 13's removeFields array. postDataRuleCleanupHelper() then strips the field on save, matching the pattern rules 6/7/8 already use for dom_req_ID.
    • Cleanup patcher patches/CleanupRule13GiftChoices.php. One-shot DELETE gc FROM gift_choices gc INNER JOIN gifts g ON g.id = gc.gift_id WHERE g.rule_id = 13 to remove rows written by previous saves.
    • Migration database/migrations/20260511120000_cleanup_rule13_gift_choices.php. Phinx migration extending \Patches\AbstractPatcher that invokes the patcher.
    • Follow-up. Advisable-com/ecommercen#229 tracks the broader UX bug (rules 6/7/8/13 form fields rendered unconditionally regardless of active rule).
    • Tests. tests/Legacy/Eshop/AdvGiftsModelTest.php 60/60 pass; pre-existing integration test errors are unrelated.

Notes

  • [4.101.0] Breaking — legacy PriceTracking classes deleted (Advisable-com/ecommercen#128):

    • Any client repo that overrode AdvTrackPrices, AdvPriceTrackingGraphs, AdvPriceTrackingOptions, AdvPriceTrackingModel, or AdvApiPriceTrackingController (or their application/modules/... thin-shim subclasses TrackPrices / PriceTrackingOptions / Price_tracking_graphs / Price_tracking_model / Api_price_tracking) must migrate to the modern domain services. Override targets:
      • Custom\Domains\Product\PriceTracking\Jobs\TrackPrices (replaces the legacy job class — extend the modern one or compose around its WriteService injection)
      • Custom\Domains\Product\PriceTracking\PriceCalculator (custom discount/VAT logic — wire via DI alias to override the upstream calculator)
      • Custom\Domains\Product\PriceTracking\GraphService (custom Omnibus reference-price algorithm)
      • Custom\Domains\Product\PriceTracking\Options (custom registry mapping)
    • Frontend callers of /api/priceTracking/... must be repointed at the modern REST endpoints: /rest/product/price-tracking/graph/{productId} (single) and /rest/product/price-tracking/graph?productIds=... (bulk).
    • Storefront/admin view templates that constructed new PriceTrackingOptions() directly now resolve \Advisable\Domains\Product\PriceTracking\Options via di()->get(...); the public property shape is preserved so view-side code reading $priceTrackingOptions->enabled, ->days, ->labelPriceTrackDiscount[...], etc. continues to work unchanged.
  • [4.101.0] Check for overrides (admin order gift validation):

    • ecommercen/eshop/controllers/Adv_orders_admin.phpvalidateProductGiftSelections() now delegates the applicable-rules filtering to Adv_gifts_model::filterApplicableGiftRules() instead of an inline array_filter. Any client override of this controller method (or of Adv_orders_admin) that retained the old inline closure will continue to drop Rule 13 gifts from the admin order edit screen. Re-point overrides at the canonical model method, or remove the override if it only existed to add the missing Rule 13 carve-out.
  • [4.101.0] REQUIRES php migrator.php migrate:

    • One-time cleanup patcher Patches\CleanupRule13GiftChoices to delete orphan rows in gift_choices for rule 13 gifts (Advisable-com/ecommercen#206).