Skip to content

Commit 3583d32

Browse files
committed
AC-13535: Minimum and maximum value validation does not work for DOB attribute on Storefront
1 parent 47721be commit 3583d32

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Customer\Plugin;
10+
11+
use Magento\Customer\Api\Data\CustomerInterface;
12+
use Magento\Customer\Api\CustomerRepositoryInterface;
13+
use Magento\Eav\Model\Config as EavConfig;
14+
use Magento\Framework\Exception\InputException;
15+
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
16+
17+
class ValidateDobOnSave
18+
{
19+
/**
20+
* @var EavConfig
21+
*/
22+
private $eavConfig;
23+
24+
/**
25+
* @var JsonSerializer
26+
*/
27+
private $json;
28+
29+
public function __construct(
30+
EavConfig $eavConfig,
31+
JsonSerializer $json
32+
) {
33+
$this->eavConfig = $eavConfig;
34+
$this->json = $json;
35+
}
36+
37+
public function aroundSave(
38+
CustomerRepositoryInterface $subject,
39+
callable $proceed,
40+
CustomerInterface $customer,
41+
$passwordHash = null
42+
) {
43+
$dobRaw = $customer->getDob();
44+
45+
$dobDate = $this->parseDate($dobRaw);
46+
if ($dobRaw !== null && $dobRaw !== '' && !$dobDate) {
47+
throw new InputException(__('Date of Birth is invalid.'));
48+
}
49+
50+
if ($dobDate) {
51+
$attr = $this->eavConfig->getAttribute('customer', 'dob');
52+
53+
$rules = $attr->getData('validate_rules');
54+
if (is_string($rules) && $rules !== '') {
55+
try {
56+
$rules = $this->json->unserialize($rules);
57+
} catch (\InvalidArgumentException $e) {
58+
$rules = [];
59+
}
60+
}
61+
if (!is_array($rules)) {
62+
$rules = (array)$attr->getValidateRules();
63+
}
64+
65+
$min = $rules['date_range_min'] ?? $rules['min_date'] ?? null;
66+
$max = $rules['date_range_max'] ?? $rules['max_date'] ?? null;
67+
68+
$minDate = $this->parseDate($min);
69+
$maxDate = $this->parseDate($max);
70+
71+
$dobKey = $dobDate->format('Y-m-d');
72+
73+
if ($minDate && $dobKey < $minDate->format('Y-m-d')) {
74+
throw new InputException(__('Date of Birth must be on or after %1.', $minDate->format('Y-m-d')));
75+
}
76+
if ($maxDate && $dobKey > $maxDate->format('Y-m-d')) {
77+
throw new InputException(__('Date of Birth must be on or before %1.', $maxDate->format('Y-m-d')));
78+
}
79+
}
80+
81+
return $proceed($customer, $passwordHash);
82+
}
83+
84+
/**
85+
* @param $value
86+
* @return \DateTimeImmutable|null
87+
* @throws \Exception
88+
*/
89+
private function parseDate($value): ?\DateTimeImmutable
90+
{
91+
if ($value === null || $value === '' || $value === false) {
92+
return null;
93+
}
94+
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
95+
$intVal = (int)$value;
96+
if ($intVal <= 0) {
97+
return null;
98+
}
99+
$seconds = ($intVal >= 10000000000) ? intdiv($intVal, 1000) : $intVal;
100+
return (new \DateTimeImmutable('@' . $seconds))->setTimezone(new \DateTimeZone('UTC'));
101+
}
102+
103+
try {
104+
return new \DateTimeImmutable((string)$value);
105+
} catch (\Exception $e) {
106+
return null;
107+
}
108+
}
109+
}

app/code/Magento/Customer/etc/frontend/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,7 @@
130130
<type name="Magento\Customer\Model\Session">
131131
<plugin name="afterLogout" type="Magento\Customer\Model\Plugin\ClearSessionsAfterLogoutPlugin"/>
132132
</type>
133+
<type name="Magento\Customer\Api\CustomerRepositoryInterface">
134+
<plugin name="customer_dobvalidation_plugin" type="Magento\Customer\Plugin\ValidateDobOnSave" sortOrder="10"/>
135+
</type>
133136
</config>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Customer\Plugin;
10+
11+
use Magento\Customer\Api\CustomerRepositoryInterface;
12+
use Magento\Customer\Api\Data\CustomerInterface;
13+
use Magento\Customer\Test\Fixture\Customer as CustomerFixture;
14+
use Magento\Eav\Model\Config as EavConfig;
15+
use Magento\Eav\Model\ResourceModel\Entity\Attribute as AttributeResource;
16+
use Magento\Framework\Exception\InputException;
17+
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
18+
use Magento\TestFramework\Fixture\DataFixture;
19+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
20+
use Magento\TestFramework\Helper\Bootstrap;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* @magentoAppArea frontend
25+
* @magentoAppIsolation enabled
26+
* @magentoDbIsolation enabled
27+
*/
28+
class ValidateDobOnSaveDataTest extends TestCase
29+
{
30+
/** @var \Magento\Framework\ObjectManagerInterface */
31+
private $om;
32+
/** @var CustomerRepositoryInterface */
33+
private $customerRepo;
34+
/** @var EavConfig */
35+
private $eavConfig;
36+
/** @var AttributeResource */
37+
private $attributeResource;
38+
/** @var JsonSerializer */
39+
private $serializer;
40+
41+
protected function setUp(): void
42+
{
43+
$this->om = Bootstrap::getObjectManager();
44+
$this->customerRepo = $this->om->get(CustomerRepositoryInterface::class);
45+
$this->eavConfig = $this->om->get(EavConfig::class);
46+
$this->attributeResource = $this->om->get(AttributeResource::class);
47+
$this->serializer = $this->om->get(JsonSerializer::class);
48+
}
49+
50+
#[
51+
DataFixture(CustomerFixture::class, as: 'cust')
52+
]
53+
public function testDobBeforeMinThrows(): void
54+
{
55+
$this->setDobRules(['date_range_min' => '1980-01-01', 'date_range_max' => '2000-12-31']);
56+
57+
$customer = $this->getCustomerFromFixture('cust');
58+
$customer->setDob('1979-12-31');
59+
60+
$this->expectException(InputException::class);
61+
$this->expectExceptionMessage('on or after 1980-01-01');
62+
$this->customerRepo->save($customer);
63+
}
64+
65+
#[
66+
DataFixture(CustomerFixture::class, as: 'cust')
67+
]
68+
public function testDobAfterMaxThrows(): void
69+
{
70+
$this->setDobRules(['date_range_min' => '1980-01-01', 'date_range_max' => '2000-12-31']);
71+
72+
$customer = $this->getCustomerFromFixture('cust');
73+
$customer->setDob('2001-01-01');
74+
75+
$this->expectException(InputException::class);
76+
$this->expectExceptionMessage('on or before 2000-12-31');
77+
$this->customerRepo->save($customer);
78+
}
79+
80+
#[
81+
DataFixture(CustomerFixture::class, as: 'cust')
82+
]
83+
public function testDobWithinRangeSucceeds(): void
84+
{
85+
$this->setDobRules(['date_range_min' => '1980-01-01', 'date_range_max' => '2000-12-31']);
86+
87+
$customer = $this->getCustomerFromFixture('cust');
88+
$customer->setDob('1990-06-15');
89+
90+
$saved = $this->customerRepo->save($customer);
91+
$this->assertNotEmpty($saved->getId());
92+
$this->assertSame('1990-06-15', $saved->getDob());
93+
}
94+
95+
#[
96+
DataFixture(CustomerFixture::class, as: 'cust')
97+
]
98+
public function testDobWithMillisecondRulesThrows(): void
99+
{
100+
$min = (new \DateTimeImmutable('1980-01-01', new \DateTimeZone('UTC')))->getTimestamp() * 1000;
101+
$max = (new \DateTimeImmutable('2000-12-31', new \DateTimeZone('UTC')))->getTimestamp() * 1000;
102+
$this->setDobRules(['date_range_min' => $min, 'date_range_max' => $max]);
103+
104+
$customer = $this->getCustomerFromFixture('cust');
105+
$customer->setDob('1979-12-31');
106+
107+
$this->expectException(InputException::class);
108+
$this->expectExceptionMessage('on or after 1980-01-01');
109+
$this->customerRepo->save($customer);
110+
}
111+
112+
#[
113+
DataFixture(CustomerFixture::class, as: 'cust')
114+
]
115+
public function testSaveWithoutDobSucceeds(): void
116+
{
117+
$this->setDobRules(['date_range_min' => '1980-01-01', 'date_range_max' => '2000-12-31']);
118+
119+
$customer = $this->getCustomerFromFixture('cust');
120+
$customer->setDob(null);
121+
122+
$saved = $this->customerRepo->save($customer);
123+
$this->assertNotEmpty($saved->getId());
124+
$this->assertNull($saved->getDob());
125+
}
126+
127+
/**
128+
* @param string $key
129+
* @return CustomerInterface
130+
* @throws \Magento\Framework\Exception\LocalizedException
131+
* @throws \Magento\Framework\Exception\NoSuchEntityException
132+
*/
133+
private function getCustomerFromFixture(string $key): CustomerInterface
134+
{
135+
$stored = DataFixtureStorageManager::getStorage()->get($key);
136+
$id = is_object($stored) && method_exists($stored, 'getId') ? (int)$stored->getId() : (int)$stored;
137+
return $this->customerRepo->getById($id);
138+
}
139+
140+
/**
141+
* @param array $rules
142+
* @return void
143+
* @throws \Magento\Framework\Exception\AlreadyExistsException
144+
* @throws \Magento\Framework\Exception\LocalizedException
145+
*/
146+
private function setDobRules(array $rules): void
147+
{
148+
$attr = $this->eavConfig->getAttribute('customer', 'dob');
149+
$attr->setData('validate_rules', $this->serializer->serialize($rules));
150+
$this->attributeResource->save($attr);
151+
$this->eavConfig->clear();
152+
}
153+
}

0 commit comments

Comments
 (0)