Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Console/Command/ItemUpdate.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function configure()
/**
* @inheritdoc
*/
public function execute(InputInterface $input, OutputInterface $output)
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->appState->setAreaCode('frontend');
$itemModel = $this->itemFactory->create();
Expand Down
2 changes: 1 addition & 1 deletion Console/Command/OrderSimulate.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public function configure()
/**
* @inheritdoc
*/
public function execute(InputInterface $input, OutputInterface $output)
public function execute(InputInterface $input, OutputInterface $output): int
{
if (!$input->getOption(self::INPUT_KEY_STORE_ID)) {
throw new \InvalidArgumentException('Please add ' . self::INPUT_KEY_STORE_ID . ' param.');
Expand Down
2 changes: 1 addition & 1 deletion Console/Command/Selftest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function configure()
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
public function execute(InputInterface $input, OutputInterface $output): int
{
$result = $this->selftestRepository->test();
foreach ($result as $test) {
Expand Down
50 changes: 50 additions & 0 deletions Plugin/Order/SkipRegionValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/**
* Copyright © Magmodules.eu. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magmodules\Channable\Plugin\Order;

use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Customer\Model\Address\Validator\Country;

/**
* Skip region validation for Channable order imports.
*
* Channable sends state_code values (e.g. "099" for LV) that don't exist in
* Magento's directory_country_region table. This plugin filters out regionId
* validation errors during Channable imports to prevent order creation failures.
*/
class SkipRegionValidation
{
private CheckoutSession $checkoutSession;

public function __construct(
CheckoutSession $checkoutSession
) {
$this->checkoutSession = $checkoutSession;
}

/**
* Filter out regionId-related validation errors during Channable order imports.
*
* @param Country $subject
* @param array $result
* @return array
*/
public function afterValidate(Country $subject, array $result): array
{
if (!$this->checkoutSession->getChannableEnabled()) {
return $result;
}

return array_values(array_filter($result, function ($error) {
$message = $error instanceof \Magento\Framework\Phrase ? $error->render() : (string)$error;
return stripos($message, 'regionId') === false
&& stripos($message, 'region_id') === false
&& stripos($message, '"region" is required') === false;
}));
}
}
50 changes: 41 additions & 9 deletions Service/Order/Quote/AddressHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ public function getAddressData(string $type, array $orderData, Quote $quote): ar
$email = $this->cleanEmail($orderData['customer']['email'] ?? '');
}

$stateCode = $address['state_code'] ?? '';
$stateName = $address['state'] ?? '';
$countryCode = $address['country_code'];
$regionId = !empty($stateCode) || !empty($stateName)
? $this->getRegionId($stateCode, $countryCode, $stateName)
: null;

$addressData = [
'customer_id' => $customerId,
'company' => $this->sanitize($company, self::PATTERN_NAME, 255),
Expand All @@ -117,10 +124,9 @@ public function getAddressData(string $type, array $orderData, Quote $quote): ar
'lastname' => $this->sanitize($address['last_name'], self::PATTERN_NAME, 255) ?? '-',
'street' => $this->getStreet($address, (int)$storeId),
'city' => $this->sanitize($address['city'], self::PATTERN_CITY, 100),
'country_id' => $address['country_code'],
'region' => !empty($address['state_code'])
? $this->getRegionId($address['state_code'], $address['country_code'])
: null,
'country_id' => $countryCode,
'region_id' => $regionId,
'region' => $regionId ? null : ($stateName ?: null),
'postcode' => $address['zip_code'],
'telephone' => $this->sanitize($telephone, self::PATTERN_TELEPHONE, 20) ?? '000',
'vat_id' => $this->getVatId($type, $orderData, $storeId),
Expand Down Expand Up @@ -267,14 +273,40 @@ public function getStreet(array $address, int $storeId): string
}

/**
* Multi-strategy region lookup:
* 1. loadByCode($code, $countryId) — e.g. "NH" + "US"
* 2. loadByCode($countryId-$code, $countryId) — e.g. "LV-099" + "LV"
* 3. loadByName($stateName, $countryId) — e.g. "Tukuma novads" + "LV"
* 4. Return null — graceful fallback
*
* @param string $code
* @param string $countryId
* @return mixed
* @param string $stateName
* @return int|null
*/
private function getRegionId(string $code, string $countryId)
private function getRegionId(string $code, string $countryId, string $stateName): ?int
{
$region = $this->regionFactory->create();
return $region->loadByCode($code, $countryId)->getId();
if (!empty($code)) {
$region = $this->regionFactory->create()->loadByCode($code, $countryId);
if ($region->getId()) {
return (int)$region->getId();
}

$prefixedCode = $countryId . '-' . $code;
$region = $this->regionFactory->create()->loadByCode($prefixedCode, $countryId);
if ($region->getId()) {
return (int)$region->getId();
}
}

if (!empty($stateName)) {
$region = $this->regionFactory->create()->loadByName($stateName, $countryId);
if ($region->getId()) {
return (int)$region->getId();
}
}

return null;
}

/**
Expand All @@ -297,7 +329,7 @@ private function saveAddress(array $addressData, int $customerId, string $type):
->setStreet(explode("\n", (string)$addressData['street']))
->setCity($addressData['city'])
->setCountryId($addressData['country_id'])
->setRegionId($addressData['region'])
->setRegionId($addressData['region_id'])
->setPostcode($addressData['postcode'])
->setVatId($addressData['vat_id'])
->setTelephone($addressData['telephone']);
Expand Down
7 changes: 5 additions & 2 deletions Service/Product/PriceData.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,11 @@ public function formatPrice($price, array $config): string
''
);

// Append currency if configured.
if ($useCurrency && $formattedPrice >= 0) {
// Clamp negative prices to 0 and always append currency if configured.
if ($useCurrency) {
if ($formattedPrice < 0) {
$formattedPrice = '0.00';
}
$formattedPrice .= ' ' . $currency;
}

Expand Down
13 changes: 13 additions & 0 deletions Test/End-2-end/support/services/ChannableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export default class ChannableApi extends BaseApi {
*/
buildOrderData(overrides: {
country?: string;
state?: string;
stateCode?: string;
price?: number;
priceTax?: number;
productId?: number;
Expand Down Expand Up @@ -105,12 +107,23 @@ export default class ChannableApi extends BaseApi {
'AT': '1010',
'BE': '1000',
'FR': '75001',
'LV': 'LV-3101',
'PL': '00-001',
};
if (zipCodes[country]) {
data.billing.zip_code = zipCodes[country];
data.shipping.zip_code = zipCodes[country];
}

if (overrides.state !== undefined) {
data.billing.state = overrides.state;
data.shipping.state = overrides.state;
}
if (overrides.stateCode !== undefined) {
data.billing.state_code = overrides.stateCode;
data.shipping.state_code = overrides.stateCode;
}

if (productId) {
data.products[0].id = productId;
}
Expand Down
20 changes: 20 additions & 0 deletions Test/End-2-end/tests/order/order-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ const testCases = [
expect(grandTotal).toBeCloseTo(12.10 * 3, 1);
},
},
{
title: 'LV order with pycountry region code',
config: {
'general/region/state_required': 'LV',
},
orderOverrides: { country: 'LV', stateCode: '099', state: 'Tukuma novads' },
assert: async (page, incrementId) => {
const displayedId = await orderViewPage.getOrderIncrementId(page);
expect(displayedId).toBeTruthy();
},
},
{
title: 'PL order without region',
config: {},
orderOverrides: { country: 'PL', stateCode: '', state: '' },
assert: async (page, incrementId) => {
const displayedId = await orderViewPage.getOrderIncrementId(page);
expect(displayedId).toBeTruthy();
},
},
{
title: 'Multi-currency order (PLN)',
config: {
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "magmodules/magento2-channable",
"description": "Channable integration for Magento 2",
"type": "magento2-module",
"version": "1.24.0",
"version": "1.25.0",
"license": "BSD-2-Clause",
"homepage": "https://github.com/magmodules/magento2-channable",
"require": {
Expand Down
2 changes: 1 addition & 1 deletion etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<general>
<enable>0</enable>
<limit>250</limit>
<version>v1.24.0</version>
<version>v1.25.0</version>
</general>
<data>
<name_attribute>name</name_attribute>
Expand Down
3 changes: 3 additions & 0 deletions etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
<type name="Amasty\ShopbySeo\Helper\Data">
<plugin name="channable_bypass_amasty_shopbyseo" type="Magmodules\Channable\Plugin\AroundIsAllowedRequest"/>
</type>
<type name="Magento\Customer\Model\Address\Validator\Country">
<plugin name="channable_skip_region_validation" type="Magmodules\Channable\Plugin\Order\SkipRegionValidation" />
</type>
<type name="Magento\Sales\Model\Order">
<plugin name="channable_pickup_location" type="Magmodules\Channable\Plugin\AfterGetShippingDescription"/>
</type>
Expand Down
Loading