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.3.0] — 2026-05-22 — Drop Magewire; render store picker via Block + Alpine.js (architectural fix)
The v1.2.1 container-name fix (checkout.shipping.before → checkout.shipping.methods.before) was necessary but not sufficient. The picker still didn't render on Hyvä Checkout because of an architectural mismatch: StorePicker extended \Magewirephp\Magewire\Component directly, but Hyvä Checkout has its own Magewire bootstrap pipeline that only hydrates components implementing its form-abstraction interfaces. Even with the container right, Hyvä's JS never bound to the block.
The "proper Magewire fix" is a meaningful research project (reading Hyvä Checkout's vendor source, implementing the right form interface, testing against Hyvä's bootstrap pipeline). v1.3.0 sidesteps that entirely by dropping Magewire — the picker doesn't actually need server-state reactivity, only visual selection-highlighting, which Alpine.js handles natively without framework lock-in.
Changed
-
New picker implementation:
Block/Checkout/PickupStorePicker.php+view/frontend/templates/checkout/pickup-store-picker.phtml. Plain Magento block (no Magewire base class) renders the active-store card list server-side. Alpine.jsx-datatracks the visually-selected card. Clicking a card callspick(code), which (1) setsselectedfor visual highlighting, and (2) finds the matching shipping-method radio (input[type=radio][value="etechflow_isp_<code>"]) and triggers itsclick()+changeevent. Standard Magento shipping commit fires, which runsShippingAddressAutofillPlugin, which overwrites the shipping address with the picked store's address (the tax-bug-kill differentiator). -
Layout update:
view/frontend/layout/hyva_checkout_components.xmlnow mounts the new block class instead of the Magewire component. Container (checkout.shipping.methods.before, retained from v1.2.1) is unchanged. -
Deprecated:
Magewire/Checkout/StorePicker.php. Marked@deprecatedin the class docblock; left on disk for backwards-compatibility with any integrator referencing it. Safe to delete in a future major release.
Why this is the right fix, not a workaround
- The picker is decorative state — Magewire's server round-trip was overkill even when it worked.
- Alpine.js works on every Magento checkout flavour that ships Alpine (Hyvä Checkout, Hyvä Theme, Luma installs with Alpine). No Hyvä-Checkout-specific bootstrap required.
- No regression to the carrier / autofill / order placement path — those continue using Magento's standard shipping-method mechanism. The picker click just simulates a radio click.
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush
docker restart <php-fpm-container>
No data migration. On next checkout, customers will see the rich card UI for the first time since v1.0.0. The bare-radio fallback path is unchanged for non-Hyvä-Checkout flows.
[1.2.1] — 2026-05-22 — CRITICAL: Magewire store picker has never rendered on Hyvä Checkout (wrong container name)
Fixed
-
Rich Magewire store-picker card UI was silently failing to mount on every Hyvä Checkout install since v1.0.0. The layout file
view/frontend/layout/hyva_checkout_components.xmlreferenced container IDcheckout.shipping.before, which doesn't exist in Hyvä Checkout — it's a stock Magento Checkout name. Hyvä Checkout has its own container vocabulary; the real shipping-area container ischeckout.shipping.methods.before. Magento's layout merger treats a<block>under a non-existent<referenceContainer>as a no-op — block created but never bound, never rendered, no error logged. Customers fell back to the plain Magento shipping-method radio list (one radio per active store: "Pick up at Keystation Maldon Store · £0.00"). The carrier still worked, orders still completed, PINs still issued — the bug was purely UX (customers saw a bare radio instead of styled card list with selected-state highlighting, instructions panel, accessibility roles).Fix: one-line change in
hyva_checkout_components.xml:- <referenceContainer name="checkout.shipping.before"> + <referenceContainer name="checkout.shipping.methods.before">Real Hyvä Checkout shipping-area containers (verified on Keystation 2.4.8 + Hyva Commerce v1.4.5):
checkout.shipping.methods.before/.aftercheckout.shipping.sectioncheckout.shipping-details.before/.section
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush
docker restart <php-fpm-container> # OPcache
No data migration. Existing orders, stores, holidays, all preserved. On next checkout, customers on Hyvä Checkout will see the rich Magewire card picker (with name + address + phone + instructions + "Selected" badge + accessible roles) instead of the bare radio fallback.
Stores not on Hyvä Checkout (plain Hyvä Theme, Luma) — unaffected. The hyva_checkout_components.xml layout handle only triggers when Hyvä Checkout is installed and active.
[1.2.0] — 2026-05-22 — Product-page "Click & Collect available" widget
Closes the visible feature gap between checkout-only ISP and competitor modules (Amasty Store Pickup, MageWorx, Wyomind) which all surface Click & Collect on the product page itself. Until now ISP only showed up at checkout — customers had no way to know C&C was available before committing to buy.
Added
-
New PDP block
ETechFlow\InStorePickup\Block\Catalog\Product\PickupAvailabilitymounted underproduct.info.priceviaview/frontend/layout/catalog_product_view.xml. Renders a small "Click & Collect available" panel with two configurable modes:Mode A — Simple notice (matches what most cheaper competitor modules ship):
🏪 Click & Collect available Pick up at any of our 3 shops at checkout. Free, same-day where stock allows.Mode B — Per-store list with live MSI stock (default — matches Amasty's headline PDP feature):
🏪 Click & Collect available ✓ Maldon (3 in stock) ✓ Chelmsford (1 in stock) ✗ Witham (out of stock) Pick a shop at checkout.Mode B iterates active pickup stores; for each store with an
msi_source_codeset (configurable on each store's admin form), queriesMagento_InventoryApi/GetSourceItemsBySkuInterfacefor the current product's per-source quantity. Stores without an MSI mapping still appear in the list, just without a stock badge. Magento_InventoryApi is soft-detected so the module survives on stripped builds where MSI is absent — Mode B silently degrades to "available at: X stores" with no stock counts. -
Admin config under
Stores → Configuration → eTechFlow → In-Store Pickup → Product Page Widget:Enable(default: Yes) — turn the PDP block on/offDisplay Mode(default: Per-store list with stock) — switch between Mode A and Mode B
Notes
This release intentionally does NOT include:
- Pre-selecting the store from PDP through to checkout (would need quote-level
pickup_preferred_store_idstorage; v1.3.0 candidate) - Cart-page Click & Collect banner (v1.3.0 candidate)
- Catalog list filter "available at my store" (heavy; deferred unless a customer asks)
- Map-based store locator (Amasty sells that as a separate paid add-on; we'd do the same)
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush
No schema changes. Pure additive — existing stores with msi_source_code unset still work, just don't show stock numbers on PDP.
[1.1.15] — 2026-05-22 — sortOrder 88 → 68 (park above Amasty)
Fixed
- eTechFlow sidebar entry not next to other vendor extensions. v1.1.14 picked
sortOrder=88expecting that to cluster with paid-extension vendors. Verified on Keystation's Hyva Commerce admin that Amasty is actually atsortOrder=69, so 88 landed between System (80) and Find Partners (100+) — not adjacent. Fix: dropped tosortOrder=68so eTechFlow sits directly above Amasty, matching the convention paid-extension vendors follow (cluster just above Stores in the same visual band).
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush
Pure menu position change — no schema, no behaviour, no admin route changes.
[1.1.14] — 2026-05-22 — Fix eTechFlow sidebar position + Configuration column grouping
Two corrections to the v1.1.13 mega-menu pilot after live-test on Keystation Hyva Commerce admin.
Fixed
-
Sidebar position parked at the bottom instead of "near Stores". v1.1.13 used
sortOrder=305expecting that to land just after Magento's Stores (typical core sortOrder ~300). Hyva Commerce admin's stock entries cap around 100, so 305 sent us to the very bottom of the sidebar under "Find Partners & Extensions". Fix: droppedsortOrderto88— clusters with the established vendor extensions (Amasty, Magefan, typically 85–89) just above Magento's Stores. Verified live: 305 → bottom; 88 → next to Amasty. -
Configuration leaf dangling at the bottom of the panel with a big vertical gap. v1.1.13 declared
eTechFlow::configurationas a direct child ofeTechFlow::root. Magento's mega-menu lays out parent-with-children entries first (as columns), then dangles leaves separately at the bottom — so Configuration appeared orphaned below "Pickup Windows" instead of grouped. Fix: introducedeTechFlow::settingsas a column header (parent=eTechFlow::root,sortOrder=500); Configuration now lives inside it (parent=eTechFlow::settings,sortOrder=10). The mega-menu renders Settings as its own column, matching how Magento's own Stores mega-menu groups its leaves under a "Settings" column rather than letting them dangle.
When other eTechFlow modules ship the same pattern, the shared eTechFlow::settings id merges into one column and each module's Configuration entry collapses into a single leaf inside it.
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush
Hyva-admin requires setup:static-content:deploy -f between di:compile and cache:flush on production-mode installs — otherwise admin pages 500 with a missing view_preprocessed/root.phtml.
No schema changes. No data migration. Pure menu-layout adjustment.
[1.1.13] — 2026-05-22 — Dedicated "eTechFlow" top-level sidebar entry (pilot)
Until now, ISP's admin pages lived inside Magento's stock Stores menu (Stores → Settings → In-Store Pickup → …). Visually indistinguishable from core menu entries — merchants couldn't tell at a glance which items were paid eTechFlow extensions vs stock Magento. Established vendors (Amasty, Magefan, MageWorx) all anchor their modules under a dedicated top-level sidebar group with their brand name. This patch starts that pattern for eTechFlow with ISP as the pilot module.
Changed
- New top-level "eTechFlow" sidebar entry. ISP's admin pages now live under a dedicated
eTechFlow::rootmenu node (sortOrder 305 — sits just after Magento's Stores, before System). The mega-menu panel opens on hover with ISP's pages (Stores / Holidays / Store Tags / Amenities / Pickup Windows) as a column. When other eTechFlow modules ship the same pattern, each contributes one column to the same panel — Magento merges entries with identical ids, so the group exists once when any eTechFlow module is installed. ETechFlow_InStorePickup::isp_rootreparented fromMagento_Backend::stores_settings→eTechFlow::root. The five submenu children (stores, holidays, tags, amenities, pickup_windows) stay parented under isp_root unchanged. No URL routes changed; only the sidebar location.- New "Configuration" leaf at the bottom of the eTechFlow sidebar group, opens
Stores → Configurationwith the eTechFlow tab pre-expanded (points to ISP's own section under<tab id="etechflow">). Visible only to users withMagento_Config::configpermission.
Migration
composer update etechflow/module-in-store-pickup
bin/magento setup:upgrade
bin/magento cache:flush
No schema changes. No data migration. Pure menu-routing change — existing admin URLs (/admin/etechflow_isp/store/index/ etc.) still work identically; only the sidebar location changed.
Notes
- No inter-module dependency added — ISP remains standalone. When you install only ISP, the eTechFlow group exists with just ISP's column. When you install all eTechFlow modules, each contributes its own column via the same shared
eTechFlow::rootid. - This is the pilot module; the remaining ETechFlow modules (Delivery Date, Back-in-Stock Notifications, Shipping Table Rates, Order Email Editor, Image Optimizer, Page Speed Optimizer) will follow the same pattern in their next releases.
[1.1.12] — 2026-05-22 — Exception Day date picker fallback to typed text
Fixed
-
Exception Day date field unusable on Hyva admin. The Exception Days dynamicRows table used Magento's standard
formElement="date"widget for the Date column. Works in standalone fieldsets (e.g. the Holiday form's date field), but on the Hyva Commerce admin theme the picker failed to initialize INSIDE the dynamicRows container — clicking the input or the calendar icon did nothing, no widget rendered, no JS error in console. Admins literally could not add an exception day. TrieddataType=text→dataType=date, addingstoreLocaleoption, cache flush + static-content:deploy — none of it landed.Fix: swapped
formElement="date"→formElement="input"+dataType=textwith aYYYY-MM-DDplaceholder and notice. Keptrequired-entry; droppedvalidate-date(the rule needs date-element metadata that text inputs don't carry → JS errors when it tries to validate). The underlyingexception_datecolumn is unchanged (still storesYYYY-MM-DDstrings).ExceptionManager::replaceRows()handles them the same way regardless of widget. Round-trip verified live: typed2026-12-25+ Closed=Yes + Reason="Christmas Day", saved, hard-reload, row reappeared.
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. Pure UI Component XML adjustment.
[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.