Skip to content

Commit 75e8631

Browse files
authored
Merge pull request #3865 from craftcms/4.8
4.8
2 parents d9f27e0 + bd5b63d commit 75e8631

34 files changed

Lines changed: 531 additions & 103 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22

33
## Unreleased
44

5+
### Store Management
6+
- Archived gateways are now listed on the Gateways index page. ([#3839](https://github.com/craftcms/commerce/issues/3839))
57
- Improved Craft Commerce navigation and breadcrumb labels.
68

9+
### Extensibility
10+
- Added support for registering custom tax ID validators.
11+
- Added `\craft\commerce\services\Taxes::getEnabledTaxIdValidators()`.
12+
- Added `\craft\commerce\services\Taxes::getTaxIdValidators()`.
13+
- Added `craft\commerce\base\TaxIdValidatorInterface`.
14+
- Added `craft\commerce\services\Gateways\getAllArchivedGateways()`.
15+
- Added `craft\commerce\services\Taxes::EVENT_REGISTER_TAX_ID_VALIDATORS`.
16+
- Added `craft\commerce\taxidvalidators\EuVatIdValidator`.
17+
718
## 4.7.3 - 2025-01-22
819

20+
- Improved the performance of recalculating shipping method prices. ([#3841](https://github.com/craftcms/commerce/pull/3841))
921
- Fixed a bug where users products had a “Save as a new product” action even if a plugin was preventing duplication via `craft\services\Elements::EVENT_AUTHORIZE_DUPLICATE`. ([#3819](https://github.com/craftcms/commerce/issues/3819))
1022
- Fixed a PHP error that could occur when updating a cart. ([#3842](https://github.com/craftcms/commerce/issues/3842))
1123
- Fixed a PHP error that could occur when adding an invalid address to a cart. ([#3848](https://github.com/craftcms/commerce/issues/3848))

example-templates/dist/shop/_private/address/fields.twig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,11 @@ Outputs address form fields for editing an address.
199199
<hr class="my-2">
200200
<div class="my-2">
201201
{{ hiddenInput('isPrimaryBilling', 0) }}
202-
<label>{{ input('checkbox', 'isPrimaryBilling', 1, { checked: address.isPrimaryBilling }) }} {{ 'Use as the primary billing address'|t('commerce') }}</label>
202+
<label>{{ input('checkbox', 'isPrimaryBilling', 1, { checked: address.isPrimaryBilling }) }} {{ 'Use as the primary billing address'|t }}</label>
203203
</div>
204204
<div class="my-2">
205205
{{ hiddenInput('isPrimaryShipping', 0) }}
206-
<label>{{ input('checkbox', 'isPrimaryShipping', 1, { checked: address.isPrimaryShipping }) }} {{ 'Use as the primary shipping address'|t('commerce') }}</label>
206+
<label>{{ input('checkbox', 'isPrimaryShipping', 1, { checked: address.isPrimaryShipping }) }} {{ 'Use as the primary shipping address'|t }}</label>
207207
</div>
208208
{% endif %}
209209
</div>
@@ -251,4 +251,4 @@ document.querySelector('select#{{ 'countryCode'|namespaceInputId(addressName) }}
251251
});
252252

253253
document.querySelector('select#{{ 'countryCode'|namespaceInputId(addressName) }}').dispatchEvent(new Event('change'));
254-
{% endjs %}
254+
{% endjs %}

example-templates/src/shop/_private/address/fields.twig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,11 @@ Outputs address form fields for editing an address.
199199
<hr class="my-2">
200200
<div class="my-2">
201201
{{ hiddenInput('isPrimaryBilling', 0) }}
202-
<label>{{ input('checkbox', 'isPrimaryBilling', 1, { checked: address.isPrimaryBilling }) }} {{ 'Use as the primary billing address'|t('commerce') }}</label>
202+
<label>{{ input('checkbox', 'isPrimaryBilling', 1, { checked: address.isPrimaryBilling }) }} {{ 'Use as the primary billing address'|t }}</label>
203203
</div>
204204
<div class="my-2">
205205
{{ hiddenInput('isPrimaryShipping', 0) }}
206-
<label>{{ input('checkbox', 'isPrimaryShipping', 1, { checked: address.isPrimaryShipping }) }} {{ 'Use as the primary shipping address'|t('commerce') }}</label>
206+
<label>{{ input('checkbox', 'isPrimaryShipping', 1, { checked: address.isPrimaryShipping }) }} {{ 'Use as the primary shipping address'|t }}</label>
207207
</div>
208208
{% endif %}
209209
</div>
@@ -251,4 +251,4 @@ document.querySelector('select#{{ 'countryCode'|namespaceInputId(addressName) }}
251251
});
252252

253253
document.querySelector('select#{{ 'countryCode'|namespaceInputId(addressName) }}').dispatchEvent(new Event('change'));
254-
{% endjs %}
254+
{% endjs %}

src/Plugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public static function editions(): array
210210
/**
211211
* @inheritDoc
212212
*/
213-
public string $schemaVersion = '4.7.0.1';
213+
public string $schemaVersion = '4.8.0.0';
214214

215215
/**
216216
* @inheritdoc

src/adjusters/Tax.php

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
use Craft;
1111
use craft\base\Component;
1212
use craft\commerce\base\AdjusterInterface;
13+
use craft\commerce\base\TaxIdValidatorInterface;
1314
use craft\commerce\elements\Order;
1415
use craft\commerce\helpers\Currency;
1516
use craft\commerce\models\OrderAdjustment;
1617
use craft\commerce\models\TaxAddressZone;
1718
use craft\commerce\models\TaxRate;
1819
use craft\commerce\Plugin;
1920
use craft\commerce\records\TaxRate as TaxRateRecord;
21+
use craft\commerce\services\Taxes;
22+
use craft\commerce\taxidvalidators\EuVatIdValidator;
2023
use craft\elements\Address;
2124
use DvK\Vat\Validator;
2225
use Exception;
@@ -35,11 +38,6 @@ class Tax extends Component implements AdjusterInterface
3538
{
3639
public const ADJUSTMENT_TYPE = 'tax';
3740

38-
/**
39-
* @var Validator|null
40-
*/
41-
private ?Validator $_vatValidator = null;
42-
4341
/**
4442
* @var Order
4543
*/
@@ -118,17 +116,17 @@ private function _adjustInternal(): array
118116
private function _getAdjustments(TaxRate $taxRate): array
119117
{
120118
$adjustments = [];
121-
$hasValidEuVatId = false;
119+
$hasValidTaxId = false;
122120

123121
$zoneMatches = $taxRate->getIsEverywhere() || ($taxRate->getTaxZone() && $this->_matchAddress($taxRate->getTaxZone()));
124122

125-
if ($zoneMatches && $taxRate->isVat) {
126-
$hasValidEuVatId = $this->organizationTaxId();
123+
if ($zoneMatches && $taxRate->hasTaxIdValidators()) {
124+
$hasValidTaxId = $this->organizationTaxIdIsValidTaxId($taxRate->getSelectedEnabledTaxIdValidators());
127125
}
128126

129127
$removeIncluded = (!$zoneMatches && $taxRate->removeIncluded);
130-
$removeDueToVat = ($zoneMatches && $hasValidEuVatId && $taxRate->removeVatIncluded);
131-
if ($removeIncluded || $removeDueToVat) {
128+
$removeDueToVatId = ($zoneMatches && $hasValidTaxId && $taxRate->removeVatIncluded);
129+
if ($removeIncluded || $removeDueToVatId) {
132130

133131
// Is this an order level tax rate?
134132
if (in_array($taxRate->taxable, TaxRateRecord::ORDER_TAXABALES, false)) {
@@ -195,7 +193,7 @@ private function _getAdjustments(TaxRate $taxRate): array
195193
return $adjustments;
196194
}
197195

198-
if (!$zoneMatches || ($taxRate->isVat && $hasValidEuVatId)) {
196+
if (!$zoneMatches || ($taxRate->hasTaxIdValidators() && $hasValidTaxId)) {
199197
return [];
200198
}
201199

@@ -323,7 +321,7 @@ private function _matchAddress(TaxAddressZone $zone): bool
323321
/**
324322
* @return bool
325323
*/
326-
private function organizationTaxId(): bool
324+
private function organizationTaxIdIsValidTaxId(array $validators): bool
327325
{
328326
if (!$this->_address) {
329327
return false;
@@ -340,7 +338,7 @@ private function organizationTaxId(): bool
340338

341339
// If we do not have a valid VAT ID in cache, see if we can get one from the API
342340
if (!$validOrganizationTaxId) {
343-
$validOrganizationTaxId = $this->validateVatNumber($this->_address->organizationTaxId);
341+
$validOrganizationTaxId = $this->validateTaxIdNumber($this->_address->organizationTaxId, $validators);
344342
}
345343

346344
if ($validOrganizationTaxId) {
@@ -355,25 +353,34 @@ private function organizationTaxId(): bool
355353
/**
356354
* @param string $businessVatId
357355
* @return bool
356+
* @deprecated in 4.8.0. Use `validateTaxIdNumber()` instead, passing the validators you want to check the ID with.
358357
*/
359358
protected function validateVatNumber(string $businessVatId): bool
359+
{
360+
$oldValidator = [new EuVatIdValidator()];
361+
return $this->validateTaxIdNumber($businessVatId, $oldValidator);
362+
}
363+
364+
/**
365+
* @param string $organizationTaxId
366+
* @param TaxIdValidatorInterface[] $validators
367+
* @return bool
368+
*/
369+
protected function validateTaxIdNumber(string $organizationTaxId, array $validators = []): bool
360370
{
361371
try {
362-
return $this->_getVatValidator()->validate($businessVatId);
372+
foreach ($validators as $validator) {
373+
if ($validator->validate($organizationTaxId)) {
374+
return true;
375+
}
376+
}
363377
} catch (Exception $e) {
364378
Craft::error('Communication with VAT API failed: ' . $e->getMessage(), __METHOD__);
365379

366380
return false;
367381
}
368-
}
369-
370-
private function _getVatValidator(): Validator
371-
{
372-
if ($this->_vatValidator === null) {
373-
$this->_vatValidator = new Validator();
374-
}
375382

376-
return $this->_vatValidator;
383+
return false;
377384
}
378385

379386
private function _createAdjustment(TaxRate $rate): OrderAdjustment
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\commerce\base;
9+
10+
/**
11+
* Interface for Tax ID Validators.
12+
*
13+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
14+
* @since 4.8.0
15+
*/
16+
interface TaxIdValidatorInterface
17+
{
18+
/**
19+
* The display name of this tax ID type.
20+
*
21+
* @return string
22+
* @since 4.8.0
23+
*/
24+
public static function displayName(): string;
25+
26+
/**
27+
* Tests if the ID looks generally correct. This would usually be something like a regex check.
28+
*
29+
* @param string $idNumber
30+
* @return bool
31+
* @since 4.8.0
32+
*/
33+
public function validateFormat(string $idNumber): bool;
34+
35+
/**
36+
* Tests if the ID exists as valid in the country's tax system. This would usually be an API call.
37+
*
38+
* @param string $idNumber
39+
* @return bool
40+
* @since 4.8.0
41+
*/
42+
public function validateExistence(string $idNumber): bool;
43+
44+
/**
45+
* This would usually just call validateFormat() and then validateExistence() and return the result.
46+
*
47+
* @param string $idNumber
48+
* @return bool
49+
* @since 4.8.0
50+
*/
51+
public function validate(string $idNumber): bool;
52+
53+
/**
54+
* Tests if the validator is available for use by tax rates.
55+
* This would usually be a check against the existence or settings or API keys so that the validator can be used.
56+
*
57+
* @return bool
58+
* @since 4.8.0
59+
*/
60+
public static function isEnabled(): bool;
61+
}

src/behaviors/ValidateOrganizationTaxIdBehavior.php

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use craft\commerce\Plugin;
77
use craft\elements\Address;
88
use craft\events\DefineRulesEvent;
9-
use DvK\Vat\Validator;
109
use Exception;
1110
use RuntimeException;
1211
use yii\base\Behavior;
@@ -16,11 +15,6 @@ class ValidateOrganizationTaxIdBehavior extends Behavior
1615
/** @var Address */
1716
public $owner;
1817

19-
/**
20-
* @var Validator
21-
*/
22-
private Validator $_vatValidator;
23-
2418
/**
2519
* @inheritdoc
2620
*/
@@ -89,23 +83,16 @@ public function validateOrganizationTaxId(): void
8983
private function _validateVatNumber(string $businessVatId): bool
9084
{
9185
try {
92-
return $this->_getVatValidator()->validate($businessVatId);
86+
$validators = Plugin::getInstance()->getTaxes()->getEnabledTaxIdValidators();
87+
foreach ($validators as $validator) {
88+
if ($validator->validate($businessVatId)) {
89+
return true;
90+
}
91+
}
9392
} catch (Exception $e) {
94-
Craft::error('Communication with VAT API failed: ' . $e->getMessage(), __METHOD__);
95-
96-
return false;
97-
}
98-
}
99-
100-
/**
101-
* @return Validator
102-
*/
103-
private function _getVatValidator(): Validator
104-
{
105-
if (!isset($this->_vatValidator)) {
106-
$this->_vatValidator = new Validator();
93+
Craft::error('Communication with Tax ID validators failed: ' . $e->getMessage(), __METHOD__);
10794
}
10895

109-
return $this->_vatValidator;
96+
return false;
11097
}
11198
}

src/console/controllers/UpgradeController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,11 @@ private function _customField(string $oldAttribute, string $label, ?string $pref
438438

439439
$field = new PlainText();
440440
$field->groupId = ArrayHelper::firstValue(Craft::$app->getFields()->getAllGroups())->id;
441-
$field->columnType = Schema::TYPE_STRING;
441+
if ($oldAttribute == 'notes') {
442+
$field->columnType = Schema::TYPE_TEXT;
443+
} else {
444+
$field->columnType = Schema::TYPE_STRING;
445+
}
442446
$field->handle = $this->prompt('Field handle:', [
443447
'required' => true,
444448
'validator' => function($handle) use ($handlePattern, $fieldsService) {

src/controllers/GatewaysController.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
namespace craft\commerce\controllers;
99

1010
use Craft;
11+
use craft\base\MissingComponentInterface;
1112
use craft\commerce\base\Gateway;
1213
use craft\commerce\base\GatewayInterface;
14+
use craft\commerce\db\Table;
1315
use craft\commerce\gateways\Dummy;
1416
use craft\commerce\helpers\DebugPanel;
1517
use craft\commerce\Plugin;
18+
use craft\db\Query;
1619
use craft\errors\DeprecationException;
20+
use craft\helpers\Html;
1721
use craft\helpers\Json;
1822
use yii\base\Exception;
1923
use yii\base\InvalidConfigException;
@@ -32,9 +36,33 @@ class GatewaysController extends BaseAdminController
3236
public function actionIndex(): Response
3337
{
3438
$gateways = Plugin::getInstance()->getGateways()->getAllGateways();
39+
$archivedGateways = Plugin::getInstance()->getGateways()->getAllArchivedGateways();
40+
41+
if (!empty($archivedGateways)) {
42+
$gatewayIdsWithTransactions = (new Query())
43+
->select(['gatewayId'])
44+
->from(Table::TRANSACTIONS)
45+
->groupBy(['gatewayId'])
46+
->column();
47+
48+
foreach ($archivedGateways as &$gateway) {
49+
$missing = $gateway instanceof MissingComponentInterface;
50+
$gateway = [
51+
'id' => $gateway->id,
52+
'title' => Craft::t('site', $gateway->name),
53+
'handle' => Html::encode($gateway->handle),
54+
'type' => [
55+
'missing' => $missing,
56+
'name' => $missing ? $gateway->expectedType : $gateway->displayName(),
57+
],
58+
'hasTransactions' => in_array($gateway->id, $gatewayIdsWithTransactions),
59+
];
60+
}
61+
}
3562

3663
return $this->renderTemplate('commerce/settings/gateways/index', [
3764
'gateways' => $gateways,
65+
'archivedGateways' => array_values($archivedGateways),
3866
]);
3967
}
4068

0 commit comments

Comments
 (0)