You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue supersedes and closes #887. It scopes the first deliverable PR for address validation in Maho, narrowed to the structural (Tier 1) layer agreed in the #887 discussion. Tier 2 (paid provider adapters) and the email-typo / mailcheck idea are spun out into separate follow-ups so each lands with the right surface area and review.
Background
#887 proposed full address validation across checkout, customer account, and admin. The discussion settled on a two-tier design:
Tier 1: free, offline, country-aware structural validation (postcode regex, required fields, subdivision membership) driven by Google libaddressinput data.
Tier 2: pluggable paid providers (Google, Postcode.eu, Smarty, Deutsche Post, etc.).
The mahocommerce/directory-data package was restructured to mirror libaddressinput's JSON under formats/<CC>.json (CC-BY 4.0, weekly auto-update). Maho does not yet depend on it, so this issue also wires the composer dependency in.
Scope decisions confirmed during planning:
Tier 1 only; provider interface deferred.
Reorder checkout fields to Country → State/Province → Postcode → City → Street → Street 2 (per @tmewes's suggestion) so country selection drives downstream field hints.
Read formats/*.json directly from the package at request time (in-process static cache). No DB import; the existing directory_country_format table keeps its current role.
Approach
Add a validator class in Mage_Directory that consumes formats/<CC>.json via Maho\DirectoryData\Paths, and wire it into Mage_Customer_Model_Address_Abstract::_basicCheck(). That method is the single chokepoint already shared by all five address flows (checkout billing, checkout shipping, customer address book, admin sales-order address, admin customer address), so no controller-level changes are needed.
Per the project guideline ("modify core files directly, avoid creating a new module unless asked"), this lands inside Mage_Directory (data) + Mage_Customer (wiring) + theme templates (reorder). No new module.
Client side: extend the existing RegionUpdater (public/js/varien/form.js and public/js/mage/adminhtml/form.js) so a country change also updates the postcode input's placeholder (from libaddressinput's zipex) and HTML5 pattern (from zip). The hint map is rendered server-side once per page; no extra XHR.
Loads each country's JSON lazily and caches in a static array.
Server-side validation wiring
app/code/core/Mage/Customer/Model/Address/Abstract.php::_basicCheck() — after existing checks, add:
Postcode regex match when a zip exists for the country. Error: "Please enter a valid postcode for {country} (example: {example})."
Subdivision-membership check when hasSubdivisions(country) and a region_id resolves to a region code. Error: "The selected state/province is not valid for the chosen country."
Existing DB-driven isRegionRequired() policy toggle is preserved; libaddressinput data is additive.
The existing customer_address_validation_after event still fires after the new checks, so merchant observers / future Tier-2 providers can stack on top.
Composer
Add "mahocommerce/directory-data": "^1.0" to composer.json.
JS
public/js/varien/form.js — extend RegionUpdater to also drive postcode placeholder + pattern on country change.
public/js/mage/adminhtml/form.js — same treatment for the admin form.
public/js/varien/opcheckout.js — pass the postcode input id when constructing billingRegionUpdater / shippingRegionUpdater.
Server-rendered hint map
app/code/core/Mage/Directory/Block/Data.php — new getAddressFormatHintsJson() method, cached per-store via Mage::app()->getCache() (tag directory_address_format, TTL 24h).
Templates rendering the country select get a one-line <script>window.mahoAddressFormatHints = ...;</script> near the country dropdown.
Field reorder (Country → State → Postcode → City → Street → Street 2)
The admin sales-order address form (programmatic via $fieldset->addField(...)) needs the same field order. PDF and address-display rendering use libaddressinput's per-country fmt string and are unaffected by input-form reordering.
Locale strings
app/locale/en_US/Mage_Customer.csv — three new entries:
"Please enter a valid postcode (example: %s)."
"Please enter a valid postcode for %s (example: %s)."
"The selected state/province is not valid for the chosen country."
Out of scope (separate follow-up issues)
Tier 2: pluggable provider interface + first reference adapter. Will be a separate issue. Per fballiano's note, shipping at least two adapters together (one global, one regional) before freezing the interface will avoid baking in a single provider's response shape.
vendor/bin/php-cs-fixer fix produces no diff in CI.
Pest backend tests cover postcode validation for IT / US / GB / DE, subdivision membership for IT, GB-without-subdivisions edge, and graceful no-op for unknown country codes.
Pest frontend integration test: POST /checkout/onepage/saveBilling with country_id=IT, postcode=0014 returns the new postcode error.
Manual smoke walk-throughs:
Guest checkout (Italy address): field order is Country → State → Postcode → City → Street; postcode input shows 00144 as placeholder; submitting 0014 is rejected with a clear message.
Customer address book: same behavior end-to-end.
Admin sales order: changing an order's address to a bogus US ZIP is rejected with the new message.
./maho cache:flush regenerates the per-store hint-map cache.
Summary
This issue supersedes and closes #887. It scopes the first deliverable PR for address validation in Maho, narrowed to the structural (Tier 1) layer agreed in the #887 discussion. Tier 2 (paid provider adapters) and the email-typo / mailcheck idea are spun out into separate follow-ups so each lands with the right surface area and review.
Background
#887 proposed full address validation across checkout, customer account, and admin. The discussion settled on a two-tier design:
The
mahocommerce/directory-datapackage was restructured to mirror libaddressinput's JSON underformats/<CC>.json(CC-BY 4.0, weekly auto-update). Maho does not yet depend on it, so this issue also wires the composer dependency in.Scope decisions confirmed during planning:
formats/*.jsondirectly from the package at request time (in-process static cache). No DB import; the existingdirectory_country_formattable keeps its current role.Approach
Add a validator class in
Mage_Directorythat consumesformats/<CC>.jsonviaMaho\DirectoryData\Paths, and wire it intoMage_Customer_Model_Address_Abstract::_basicCheck(). That method is the single chokepoint already shared by all five address flows (checkout billing, checkout shipping, customer address book, admin sales-order address, admin customer address), so no controller-level changes are needed.Per the project guideline ("modify core files directly, avoid creating a new module unless asked"), this lands inside
Mage_Directory(data) +Mage_Customer(wiring) + theme templates (reorder). No new module.Client side: extend the existing
RegionUpdater(public/js/varien/form.jsandpublic/js/mage/adminhtml/form.js) so a country change also updates the postcode input'splaceholder(from libaddressinput'szipex) and HTML5pattern(fromzip). The hint map is rendered server-side once per page; no extra XHR.Implementation plan
New
app/code/core/Mage/Directory/Model/AddressFormat.php— validator with:validatePostcode(string $countryCode, ?string $postcode): bool(anchors thezipregex)validateSubdivision(string $countryCode, ?string $regionCode): bool(membership insub_keys)getRequiredFieldCodes(string $countryCode): arraygetPostcodeExample(string $countryCode): ?stringgetPostcodePattern(string $countryCode): ?stringhasSubdivisions(string $countryCode): boolgetClientHints(): array(country -> {example, pattern, hasSubdivisions, requiredCodes} map)Server-side validation wiring
app/code/core/Mage/Customer/Model/Address/Abstract.php::_basicCheck()— after existing checks, add:zipexists for the country. Error: "Please enter a valid postcode for {country} (example: {example})."hasSubdivisions(country)and aregion_idresolves to a region code. Error: "The selected state/province is not valid for the chosen country."isRegionRequired()policy toggle is preserved; libaddressinput data is additive.customer_address_validation_afterevent still fires after the new checks, so merchant observers / future Tier-2 providers can stack on top.Composer
"mahocommerce/directory-data": "^1.0"tocomposer.json.JS
public/js/varien/form.js— extendRegionUpdaterto also drive postcodeplaceholder+patternon country change.public/js/mage/adminhtml/form.js— same treatment for the admin form.public/js/varien/opcheckout.js— pass the postcode input id when constructingbillingRegionUpdater/shippingRegionUpdater.Server-rendered hint map
app/code/core/Mage/Directory/Block/Data.php— newgetAddressFormatHintsJson()method, cached per-store viaMage::app()->getCache()(tagdirectory_address_format, TTL 24h).<script>window.mahoAddressFormatHints = ...;</script>near the country dropdown.Field reorder (Country → State → Postcode → City → Street → Street 2)
app/design/frontend/base/default/template/checkout/onepage/billing.phtmlapp/design/frontend/base/default/template/checkout/onepage/shipping.phtmlapp/design/frontend/base/default/template/checkout/onestep.phtmlapp/design/frontend/base/default/template/customer/address/edit.phtmlapp/design/adminhtml/default/default/template/customer/tab/addresses.phtml$fieldset->addField(...)) needs the same field order. PDF and address-display rendering use libaddressinput's per-countryfmtstring and are unaffected by input-form reordering.Locale strings
app/locale/en_US/Mage_Customer.csv— three new entries:"Please enter a valid postcode (example: %s).""Please enter a valid postcode for %s (example: %s).""The selected state/province is not valid for the chosen country."Out of scope (separate follow-up issues)
zipexvalue).Acceptance criteria / verification
composer require mahocommerce/directory-data:^1.0resolves cleanly.vendor/bin/phpstan analyzepasses at level 6.vendor/bin/php-cs-fixer fixproduces no diff in CI./checkout/onepage/saveBillingwithcountry_id=IT, postcode=0014returns the new postcode error.00144as placeholder; submitting0014is rejected with a clear message../maho cache:flushregenerates the per-store hint-map cache.composer testis green.Closes #887.