etechflow / module-in-store-pickup
etechflow/module-in-store-pickup
Click & Collect / in-store pickup for Magento 2. Multi-store admin (stores, holidays, tags, amenities, pickup windows). Hyvä Checkout Magewire-native. Auto-fills shipping address from picked store (kills the wrong-tax bug). Standalone — works without any other ETechFlow module — and integrates richer when paired with NDE / DD / BED.
ETechFlow_InStorePickup
Click & Collect for Magento 2. Multi-store admin (stores, holidays, tags, amenities, pickup windows). Hyvä Checkout Magewire-native. Auto-fills shipping address from the picked store — the killer feature every other C&C module misses.
Status: v1.0.0 Phase 1 (foundation only — admin CRUD ships in Phase 2-4).
Why this exists
Every existing Magento 2 C&C module shares the same structural bug: when a customer picks "in-store collection", they're still required to fill in a shipping address. They type their home address, Magento charges tax based on it, the merchant has to manually fix every order. We searched the market and 8+ vendors all have this same problem.
ETechFlow_InStorePickup solves it: when the customer picks a store, the shipping address auto-fills with the store's address. Tax calculation works. Order export works. Refund routing works. No manual cleanup.
Features
| Multi-store admin (stores, hours, holidays, tags, amenities, pickup windows) | Phase 2-4 |
| Auto-fill shipping address from picked store | Phase 6 |
| Hyvä Checkout Magewire-native store picker | Phase 7 |
| Hyvä Theme + Luma fallback templates | Phase 7 |
| Per-installation HMAC license + bundle key | ✅ Phase 1 |
Verify CLI (etechflow:isp:verify) |
✅ Phase 1 |
Tideways profiler instrumentation (ETechFlow_ISP_* spans) |
✅ Phase 1 |
| Pickup-ready email + 6-digit verification code | Phase 8 |
| Optional NDE / DD / BED integrations | Phase 9 |
Holiday import CLI (etechflow:isp:import-holidays) |
Phase 10 |
Standalone — and better when paired
This module works fully standalone. It does NOT require any other eTechFlow module.
When the sibling modules ARE installed, soft-detection kicks in:
| Optional pairing | Standalone | With pairing |
|---|---|---|
| + NDE | Reads MSI stock | Uses NDE's eligibility engine (drop-ship + supplier rules) |
| + DD | Own pickup windows | Optional: reuse DD's time intervals (consolidated UX) |
| + BED | Generic "available once restocked" | Per-product backorder ETA shown on pickup options |
Compatibility
| Platform | Status |
|---|---|
| Magento Open Source 2.4.4 – 2.4.8 | ✓ |
| Adobe Commerce 2.4.4 – 2.4.8 | ✓ |
| Hyvä Theme + Hyvä Checkout | ✓ Magewire-native |
| PHP 8.1 / 8.2 / 8.3 / 8.4 | ✓ |
Installation
composer require etechflow/module-in-store-pickup:^1.0
bin/magento module:enable ETechFlow_InStorePickup
bin/magento setup:upgrade
bin/magento setup:di:compile # production mode only
bin/magento cache:flush
setup:upgrade creates 11 tables prefixed etechflow_isp_.
Smoke test
bin/magento etechflow:isp:verify
Phase 1 verify confirms license + config + all 11 tables exist.
License
Proprietary — see LICENSE.txt. Commercial licenses available at https://etechflow.com.
Changelog — ETechFlow In-Store Pickup
All notable changes to this module. Adheres to Semantic Versioning.
[1.1.11] — 2026-05-22 — Pickup Window Overrides + Exception Days dynamicRows wrap/unwrap
Two <dynamicRows> fieldsets on the Store edit form ("Pickup Window Overrides" and "Exception Days") had been broken in opposite directions: new rows appeared to save (success toast) but were silently dropped, and saved rows existed in DB but rendered blank on reload. Both stem from the same Magento UI Component contract, only half-implemented.
Fixed
-
New
window_overrides/exceptionsrows silently skipped on save. POST payload fromdynamicRowsarrives nested under the component's dataScope:{"window_overrides": {"window_overrides": [...rows]}}. The Save controller iterated the outer dict as the row list, got the inner array as a "row",$row['window_id']/$row['exception_date']was undefined → 0 → the no-op skip inWindowOverrideManager::replaceRows()/ExceptionManager::replaceRows()silently dropped every row. Fix:Store\Save::unwrapDynamicRows($data, $key)strips one level of dataScope nesting before passing to the manager. If$data[<key>]exists and is an array, returns it; otherwise returns$dataas-is. Used for bothexceptionsandwindow_overrides. -
Saved
window_overrides/exceptionsrows invisible on reload. The DataProvider returned rows as a flat array under their dataScope. ThedynamicRowscomponent's record-template adapter binds existing rows via a wrapped shape AND requires each row to have a uniquerecord_id— without either, it sees data it doesn't recognise and renders zero rows even though the DB has them. Fix:Ui\Component\Form\DataProvider::prepareDynamicRows($rows, $boolField)reindexes the rows, assignsrecord_id = $iper row, and casts the bool column to int 0/1 (matches the select valueMap). Output is then wrapped:$row['<scope>'] = ['<scope>' => $rows]. Now the DataProvider-emit shape and Save-receive shape are reciprocal.
Unchanged
- DB schema and the no-op skip logic in
WindowOverrideManager/ExceptionManager(the skip was correct — it just wasn't seeing real rows). v1.1.10's amenity/tag multiselect string-cast and Open/Closed source model both still apply.
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
docker exec <php-fpm-container> kill -USR2 1 # restart php-fpm — clears OPcache
No schema changes. No data migration. Pure controller + DataProvider layer.
[1.1.10] — 2026-05-22 — Open/Closed labels + amenity/tag multiselect highlight fix
Two related "saved but doesn't look saved" admin-form polish fixes, both rooted in Magento UI Component strict-equality semantics.
Changed
is_closeddropdowns now show "Open / Closed" instead of "Yes / No". v1.1.8 replaced the broken toggle sliders with Yes/No selects — that landed but Keystation flagged a UX issue: a "Yes / No" dropdown above the weekday rows reads ambiguously ("Yes" parses as a confirmation, not as "yes, closed"). Especially in the exception_days dynamicRows where rows have no row-level label. IntroducedETechFlow\InStorePickup\Model\Source\OpenClosedreturning the same int 0/1 the column has always stored (value=0 → "Open", value=1 → "Closed"). Wired into the 9 controls that holdis_closed: 7 weekday selects + exception_days select in the Store form, the holiday Closed-All-Day select + listing column. Other Yes/No fields (is_active on store/amenity/tag, is_disabled on window_overrides, is_recurring on holiday) untouched — Yes/No is correct there. DB column, save handler, imports/visibility links all unchanged — labels-only.
Fixed
- Amenity / Tag multiselect: saved IDs not highlighted on reload. Saving correctly persisted N amenity IDs, but reopening the Store form showed zero selections highlighted. Same strict-equality category as the v1.1.7 single-checkbox bug — opposite direction:
AmenityOptions/TagOptionsreturnedvalue=(int)$id.DataProviderreturned the saved-IDs array asint[](AssignmentManager::getAssigned()casts viaintval).- But the multiselect compares the selected-IDs array against option values with
===, and POST submits IDs as strings. int 1 !== string "1"→ nothing pre-selected on reload.- Fix: cast to string on BOTH sides.
AmenityOptions::toOptionArray()andTagOptions::toOptionArray()now emitvalue=(string)$id;Ui/Component/Form/DataProvider::getData()wraps bothassigned_amenity_ids/assigned_tag_idswitharray_map('strval', …). Docblocks on both Options classes flag the strict-equality reason so the cast doesn't get "cleaned up" later.
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
docker exec <php-fpm-container> kill -USR2 1 # or restart php-fpm — clears OPcache
No schema changes. No data migration. Pure UI / DataProvider layer.
[1.0.1] — 2026-05-20 — Install-on-Hyvä hotfix
Two real bugs surfaced during the first install on a Hyvä client store. Both fixed.
Fixed
ShippingAddressAutofillPlugin::afterSetShippingMethodstrict return type. The method signature used: Addressstrict return type and unconditionally returned$result. If any third-party plugin earlier in the plugin chain returnednull/false/ a non-Addressvalue (legal under Magento's plugin contract), our plugin would emit aTypeErrormid-response. Because the fatal happened after output buffering started, nginx saw a 200 status with malformed chunked output — andvar/log/exception.logstayed empty because a TypeError isn't an Exception. Symptom: admin/category pages randomly broken with "Backend fetch failed" while no error appeared in logs.- Fix: removed the strict return type. Added an
instanceof Addressguard before our autofill logic runs. Non-Address$resultvalues now pass through unchanged.
- Fix: removed the strict return type. Added an
magewirephp/magewirewas insuggestinstead ofrequire. TheMagewire/Checkout/StorePicker.phpclass extends\Magewirephp\Magewire\Component. On a store without Magewire installed (Hyvä Theme without Hyvä Checkout, or a clean Open Source install),bin/magento setup:di:compilewould attempt to compile the class against the missing parent and either fail or produce broken interceptors ingenerated/code/. The CHANGELOG entry for v1.0.0 incorrectly claimed the file would "sit inert on disk" — di:compile scans every class regardless.- Fix: moved
magewirephp/magewire: ^1.0||^2.0intorequire. Composer will now install it transitively (it's a tiny package — single PHP file plus a few helper classes — and it's the parent of every Hyvä Checkout component anyway).
- Fix: moved
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
docker exec <php-fpm-container> kill -USR2 1 # or restart php-fpm — clears OPcache
If you're upgrading from v1.0.0 on a host with opcache.validate_timestamps=0, you MUST restart php-fpm after setup:di:compile — otherwise stale compiled interceptors stay in memory and you see the same "two workers returning different output sizes" symptom that masked Bug 1.
[1.0.0] — 2026-05-20 — Click & Collect for Magento 2
First commercial release. Click & Collect / in-store pickup module with the differentiators competing modules can't ship: auto-fill shipping address from the picked store (kills the wrong-tax bug 8+ competitors all have) and a standalone-first architecture that gets richer when paired with the rest of the eTechFlow suite.
The differentiator competitors can't match
Every existing Magento 2 C&C module (Amasty, MageWorx, WebKul, Magenest, Wyomind, Fooman, MageDelight, Mageants, Setubridge, FME, Meetanshi…) bolts onto Magento's shipping system, which requires a shipping address. So they all force the customer to type one when picking C&C. Customers type their home address, Magento charges tax based on it, the merchant has to manually fix every order.
ETechFlow_InStorePickup solves this: when the customer picks a pickup method, our plugin overwrites the shipping address with the store's address — silently. Tax calculation is then correct out of the box. No manual cleanup.
Added
Foundation
registration.php,composer.json(proprietary licence, soft-dependencies on NDE / DD / BED / Hyvä Checkout)etc/module.xmlsetup_version1.0.0- DB schema (
etc/db_schema.xml) — 11 tables: stores, hours, exceptions, holidays, store_holiday_exclusion, amenities, store_amenity, tags, store_tag, pickup_windows, store_pickup_windows. All FKs CASCADE on store delete. Indexes on hot-path columns. - Admin config (
etc/adminhtml/system.xml) — License section + General Settings + eTechFlow Suite Integrations + Notifications, all with plain-English tooltips.
Licensing + Module Infrastructure
Model/LicenseValidator— per-domain HMAC + bundle key.MODULE_ID = in-store-pickup. Shares BUNDLE_SECRET with every eTechFlow module.Model/Config— license-awareisEnabled(). Soft-detection of optional integrations (NDE, DD, BED) viaclass_exists.Model/Performance/Profiler— Tideways span helper, tagsETechFlow_ISP_*.
Entities + Service Contracts
- Full Api/Data + Api/Repository contracts for 5 entities: Store, Tag, Amenity, PickupWindow, Holiday. Every method has a docblock (per our standards).
- Model + ResourceModel + Collection + Repository implementations for all 5.
Stores Admin (CRUD)
- Magento UI Component listing grid (search, filters, sortable columns, bookmarks, mass actions: Delete / Enable / Disable, inline edit on name/active/sort_order)
- Tabbed edit form (General / Address / About & Contact)
- 9 controllers: Index, NewAction, Edit, Save, Delete, InlineEdit, MassDelete, MassEnable, MassDisable
- Form action buttons (Save / Save & Continue / Back / Delete), all license-aware
Shipping Carrier
Model/Carrier/InStorePickup— registered asetechflow_isp. Returns one method per active store (Pick up at <store name>at £0).- Standard
getAllowedMethods()+collectRates()pattern. Safe-fails to "no rates" on any error so checkout never breaks.
THE KILLER FEATURE
Plugin/Quote/ShippingAddressAutofillPlugin—after-plugin onMagento\Quote\Model\Quote\Address::setShippingMethod. When the method matchesetechflow_isp_<store_code>, overwrites the customer's shipping address with the store's address. Forces tax recalculation. Solves the universal C&C wrong-tax bug.
Notifications
Model/PickupOrderDetector— central helper for "is this order a pickup?" + "which store?"Model/PickupCodeGenerator— cryptographically random 4-8 digit pickup-verification codes (length configurable)Model/Notification/StaffAlertSender+view/frontend/email/staff_alert.html— emails the staff at the picked store when a new pickup order arrives.Observer/StaffAlertObserveronsales_order_place_after— fires the alert. Pickup orders only; non-pickup short-circuits at zero cost.
Optional eTechFlow Suite Adapters
Model/Adapter/NdeEligibilityAdapter— when NDE is installed + admin opted in, ISP defers to NDE's stock-eligibility rules engine (drop-ship + supplier mode + force-standard overrides). Falls back to MSI source-stock when NDE isn't present.
Verify CLI
bin/magento etechflow:isp:verify— 13 checks covering license, config, all 11 DB tables, all 5 repositories via DI, carrier instantiation, auto-fill plugin presence. Exit 0 on full pass, 1 on any failure.
Standalone-first architecture
This module works fully standalone. The integrations with NDE / DD / BED are opt-in enhancements soft-detected via class_exists — if the sibling module isn't installed, ISP falls back to its own self-contained logic.
| Optional pairing | Standalone | With pairing |
|---|---|---|
| + NDE | Reads MSI source stock directly | Uses NDE's rules engine (drop-ship + supplier mode + force-standard) |
| + DD | Own simple pickup windows | Optional: reuse DD's time intervals (consolidated UX) |
| + BED | Generic "available once restocked" | Per-product backorder ETA on pickup options |
Compatibility
- Magento Open Source 2.4.4 – 2.4.8
- Adobe Commerce 2.4.4 – 2.4.8
- PHP 8.1 / 8.2 / 8.3 / 8.4
- Hyvä Theme + Hyvä Checkout (Magewire integration ships in v1.1)
Known limitations (deferred to v1.1)
- Tag / Amenity / Pickup Window / Holiday admin UIs — entities exist; admin grids ship in v1.1. Configure these via direct DB seed or via the Holiday import CLI for v1.0.
- Per-store Hours / Exception days editor as sub-tabs on Store form — schema is in place; admin sub-tabs ship in v1.1. Configure via direct DB seed for v1.0.
- Customer "Pickup Ready" email + admin "Mark Ready" button — needs schema columns on
sales_orderfor pickup_code + pickup_status; deferred to v1.1. - Hyvä Checkout Magewire-native store picker — DD already has a Magewire date picker (v1.4.0); same pattern applies here in v1.1. v1.0 customers using Hyvä Checkout get the standard Magento checkout's shipping-method radio list, with one method per store.
- Holiday import CLI (
etechflow:isp:import-holidays --country=GB) — deferred to v1.1. - Map view (Leaflet, lazy-loaded) — v1.2.
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 2.2.0 | stable | Fail | Magento 2.4.7-2.4.8 Details | 2026-06-06 14:14:39 |
| 2.1.2 | stable | Not tested | Not yet tested Details | 2026-06-04 20:21:29 |
| 2.0.2 | stable | Not tested | Not yet tested Details | 2026-06-04 19:54:12 |
| 1.3.2 | stable | Not tested | Not yet tested Details | 2026-06-04 19:38:19 |
| 2.1.1 | stable | Not tested | Not yet tested Details | 2026-06-03 10:08:42 |
| 2.1.0 | stable | Not tested | Not yet tested Details | 2026-06-03 08:18:01 |
| 2.0.1 | stable | Not tested | Not yet tested Details | 2026-05-26 16:35:48 |
| 2.0.0 | stable | Not tested | Not yet tested Details | 2026-05-26 16:16:15 |
| 1.3.1 | stable | Not tested | Not yet tested Details | 2026-05-22 11:11:01 |
| 1.3.0 | stable | Not tested | Not yet tested Details | 2026-05-22 10:18:08 |
| 1.2.1 | stable | Not tested | Not yet tested Details | 2026-05-22 09:54:50 |
| 1.2.0 | stable | Not tested | Not yet tested Details | 2026-05-22 09:26:30 |
| 1.1.15 | stable | Not tested | Not yet tested Details | 2026-05-22 06:12:14 |
| 1.1.14 | stable | Not tested | Not yet tested Details | 2026-05-22 05:50:20 |
| 1.1.13 | stable | Not tested | Not yet tested Details | 2026-05-22 05:13:16 |
| 1.1.12 | stable | Not tested | Not yet tested Details | 2026-05-21 22:21:30 |
| 1.1.11 | stable | Not tested | Not yet tested Details | 2026-05-21 22:02:24 |
| 1.1.10 | stable | Not tested | Not yet tested Details | 2026-05-21 21:16:14 |
| 1.1.9 | stable | Not tested | Not yet tested Details | 2026-05-21 20:06:23 |
| 1.1.8 | stable | Not tested | Not yet tested Details | 2026-05-21 19:52:51 |
| 1.1.7 | stable | Not tested | Not yet tested Details | 2026-05-21 19:09:18 |
| 1.1.6 | stable | Not tested | Not yet tested Details | 2026-05-21 18:18:08 |
| 1.1.5 | stable | Not tested | Not yet tested Details | 2026-05-21 17:56:32 |
| 1.1.4 | stable | Not tested | Not yet tested Details | 2026-05-21 16:53:10 |
| 1.1.2 | stable | Not tested | Not yet tested Details | 2026-05-21 16:02:13 |
| 1.1.1 | stable | Not tested | Not yet tested Details | 2026-05-21 14:56:46 |
| 1.1.0 | stable | Not tested | Not yet tested Details | 2026-05-21 14:18:12 |
| 1.0.1 | stable | Not tested | Not yet tested Details | 2026-05-20 16:09:21 |
Requires 13
| Package | Constraint |
|---|---|
| magento/framework | ^103.0||^104.0 |
| magento/module-backend | ^102.0||^103.0 |
| magento/module-catalog-inventory | ^100.4||^101.0 |
| magento/module-config | ^101.2||^102.0 |
| magento/module-customer | ^103.0||^104.0 |
| magento/module-eav | ^102.1||^103.0 |
| magento/module-quote | ^101.2||^102.0 |
| magento/module-sales | ^103.0||^104.0 |
| magento/module-shipping | ^100.4||^101.0 |
| magento/module-store | ^101.1||^102.0 |
| magento/module-ui | ^101.2||^102.0 |
| magewirephp/magewire | ^1.0||^2.0 |
| php | ~8.1.0||~8.2.0||~8.3.0||~8.4.0 |
Suggests 4
| Package | Reason |
|---|---|
| etechflow/module-backorder-eta-display | Optional. When installed, the pickup-ready date for backorder items pulls from BED's per-product ETA. |
| etechflow/module-delivery-date | Optional. When installed, ISP can reuse DD's time intervals as pickup-window slots — consolidated admin UX. |
| etechflow/module-next-day-eligibility | Optional. When installed, ISP uses NDE's stock-eligibility rules engine for richer per-product picklability checks (drop-ship, supplier mode, force-standard overrides). |
| hyva-themes/magento2-hyva-checkout | Hyvä Checkout activates the Magewire-native store picker. Plain Hyvä Theme / Luma use Alpine + server-rendered fallback. |
No QA results yet
QA pipelines haven't run for this version. Compatibility and quality results appear here once the vendor publishes a tagged release that gets ingested.
More from etechflow
View vendorDynamic 'View Other Options/Finishes/Sizes' PDP buttons driven by per-product link attributes; replaces hardcoded in-description buttons and strips the old ones at render time.
Theme-agnostic mega menu for Magento 2. Renders on Hyvä, Luma, Adobe Commerce default and custom themes via automatic runtime detection. Provides a JSON endpoint for lazy-loaded subcategory + featured-product data.
EtechFlow Store Locator — admin-managed store/branch finder for Magento 2 with a Leaflet + OpenStreetMap map and postcode proximity search (postcodes.io). Hyva and Luma compatible. No paid map API key required.
Universal Product Fitment Finder for Magento 2 — Make/Model/Year/Part filtering on any fitment domain (automotive, motorcycle, marine, RV, phone cases, watches, appliance parts, anywhere a customer asks "will this fit my X?"). Admin-configurable labels + URL prefix so the same module rebrands to any merchant domain. Includes PDP fitment badge, SEO URLs, customer garage with cross-device sync, OEM/part-number search, and admin tooltips throughout. Theme-agnostic — Hyvä, Luma, custom themes. Renamed from "module-vehicle-compat" in v2.0.0.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.