Create at least one reminder if it falls to past (#3706)

This commit is contained in:
Yurii Kuznietsov
2026-06-25 16:55:10 +03:00
committed by GitHub
parent 9f9da4fc1e
commit be9323bca2
5 changed files with 203 additions and 21 deletions

View File

@@ -269,6 +269,11 @@ class Binding implements BindingProcessor
private function bindMisc(Binder $binder): void
{
$binder->bindImplementation(
'Espo\\Core\\Utils\\DateTime\\Clock',
'Espo\\Core\\Utils\\DateTime\\SystemClock'
);
$binder->bindImplementation(
'Espo\\Core\\Utils\\Id\\RecordIdGenerator',
'Espo\\Core\\Utils\\Id\\DefaultRecordIdGenerator'

View File

@@ -31,6 +31,7 @@ namespace Espo\Core\FieldProcessing\Reminder;
use Espo\Core\Field\DateTime;
use Espo\Core\Name\Field;
use Espo\Core\Utils\DateTime\Clock;
use Espo\Core\Utils\Id\RecordIdGenerator;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Preferences;
@@ -42,7 +43,6 @@ use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\FieldProcessing\Saver as SaverInterface;
use Espo\Core\FieldProcessing\Saver\Params;
use Espo\Core\ORM\EntityManager;
use stdClass;
/**
@@ -58,7 +58,8 @@ class Saver implements SaverInterface
private EntityManager $entityManager,
private RecordIdGenerator $idGenerator,
private User $user,
private Metadata $metadata
private Metadata $metadata,
private Clock $clock,
) {}
public function process(Entity $entity, Params $params): void
@@ -119,14 +120,12 @@ class Saver implements SaverInterface
$this->getPreferencesReminderList($typeList, $userId, $entityType) :
$this->getReminderList($entity, $typeList);
foreach ($reminderList as $item) {
$this->createReminder($entity, $userId, $start, $item);
}
$this->createReminders($reminderList, $entity, $userId, $start);
}
}
/**
* @return object{seconds: int, type: string}[]
* @return array{seconds: int, type: string}[]
*/
private function getEntityReminderDataList(CoreEntity $entity): array
{
@@ -146,7 +145,7 @@ class Saver implements SaverInterface
->find();
foreach ($collection as $reminder) {
$dataList[] = (object) [
$dataList[] = [
'seconds' => $reminder->getSeconds(),
'type' => $reminder->getType(),
];
@@ -282,7 +281,7 @@ class Saver implements SaverInterface
/**
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
* @return array{seconds: int, type: string}[]
*/
private function getReminderList(CoreEntity $entity, array $typeList): array
{
@@ -302,7 +301,7 @@ class Saver implements SaverInterface
/**
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
* @return array{seconds: int, type: string}[]
*/
private function getPreferencesReminderList(array $typeList, string $userId, string $entityType): array
{
@@ -328,7 +327,7 @@ class Saver implements SaverInterface
/**
* @param stdClass[] $list
* @param string[] $typeList
* @return object{seconds: int, type: string}[]
* @return array{seconds: int, type: string}[]
*/
private function sanitizeList(array $list, array $typeList): array
{
@@ -342,7 +341,7 @@ class Saver implements SaverInterface
continue;
}
$result[] = (object) [
$result[] = [
'seconds' => $seconds,
'type' => $type,
];
@@ -352,22 +351,28 @@ class Saver implements SaverInterface
}
/**
* @param object{seconds: int, type: string} $item
* @param array{seconds: int|null, type: string} $item
*/
private function createReminder(
CoreEntity $entity,
string $userId,
DateTime $start,
object $item
): void {
array $item,
): bool {
$seconds = $item->seconds;
$type = $item->type;
$seconds = $item['seconds'];
$type = $item['type'];
$remindAt = $start->addSeconds(- $seconds);
$now = DateTime::fromDateTime($this->clock->now());
if ($remindAt->isLessThan(DateTime::createNow())) {
return;
if ($seconds !== null) {
$remindAt = $start->addSeconds(- $seconds);
if ($remindAt->isLessThan($now)) {
return false;
}
} else {
$remindAt = $now;
}
$query = $this->entityManager
@@ -397,6 +402,8 @@ class Saver implements SaverInterface
->build();
$this->entityManager->getQueryExecutor()->execute($query);
return true;
}
private function toRemove(CoreEntity $entity): bool
@@ -435,4 +442,70 @@ class Saver implements SaverInterface
return in_array($statusFetched, $ignoreStatusList) && !in_array($status, $ignoreStatusList);
}
/**
* @param array{seconds: int, type: string}[] $list
*/
private function createReminders(
array $list,
CoreEntity $entity,
string $userId,
DateTime $start,
): void {
if ($list === []) {
return;
}
$hasPopup = array_filter($list, fn ($it) => $it['type'] === Reminder::TYPE_POPUP) !== [];
$hasEmail = array_filter($list, fn ($it) => $it['type'] === Reminder::TYPE_EMAIL) !== [];
$hasPopupCreated = false;
$hasEmailCreated = false;
foreach ($list as $item) {
$isCreated = $this->createReminder(
entity: $entity,
userId: $userId,
start: $start,
item: $item,
);
if (!$isCreated) {
continue;
}
if ($item['type'] === Reminder::TYPE_POPUP) {
$hasPopupCreated = true;
}
if ($item['type'] === Reminder::TYPE_EMAIL) {
$hasEmailCreated = true;
}
}
if ($hasPopup && !$hasPopupCreated) {
$this->createReminder(
entity: $entity,
userId: $userId,
start: $start,
item: [
'type' => Reminder::TYPE_POPUP,
'seconds' => null,
],
);
}
if ($hasEmail && !$hasEmailCreated) {
$this->createReminder(
entity: $entity,
userId: $userId,
start: $start,
item: [
'type' => Reminder::TYPE_EMAIL,
'seconds' => null,
],
);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\DateTime;
use Psr\Clock\ClockInterface;
/**
* @since 10.0.0
*/
interface Clock extends ClockInterface
{}

View File

@@ -30,13 +30,12 @@
namespace Espo\Core\Utils\DateTime;
use Espo\Core\Field\DateTime;
use Psr\Clock\ClockInterface;
use DateTimeImmutable;
/**
* @since 9.1.0
*/
class SystemClock implements ClockInterface
class SystemClock implements Clock
{
public function now(): DateTimeImmutable
{

View File

@@ -29,9 +29,15 @@
namespace tests\integration\Espo\Core\FieldProcessing;
use DateTimeImmutable;
use Espo\Core\Authentication\Util\DelayUtil;
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\BindingProcessor;
use Espo\Core\Field\DateTime;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\DateTime\Clock;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Meeting;
use Espo\Modules\Crm\Entities\Reminder;
use tests\integration\Core\BaseTestCase;
@@ -70,4 +76,65 @@ class ReminderTest extends BaseTestCase
$this->assertEquals(2, count($reminderList));
}
public function testFallsToPast(): void
{
$clock = $this->createMock(Clock::class);
$clock->method('now')
->willReturn(new DateTimeImmutable('2030-01-01 00:00'));
$app = $this->createApplication(
binding: new class ($clock) implements BindingProcessor {
public function __construct(private Clock $clock) {}
public function process(Binder $binder): void
{
$binder->bindInstance(Clock::class, $this->clock);
}
},
// @todo Need to reset loaded hooks in the HookManager. Bind EventDispatcher to create repositories,
// so that it is available in the HookManager.
//reuse: true,
);
$this->setApplication($app);
$entityManager = $this->getEntityManager();
$user = $this->getContainer()->getByClass(User::class);
$meeting = $entityManager->createEntity(Meeting::ENTITY_TYPE, [
'dateStart' => DateTime::fromDateTime($clock->now())->modify('+10 minutes')->toString(),
'usersIds' => [$user->getId()],
'reminders' => [
(object) [
'type' => Reminder::TYPE_POPUP,
'seconds' => 60 * 60,
],
(object) [
'type' => Reminder::TYPE_POPUP,
'seconds' => 120 * 60,
],
(object) [
'type' => Reminder::TYPE_EMAIL,
'seconds' => 60 * 60,
],
(object) [
'type' => Reminder::TYPE_EMAIL,
'seconds' => 120 * 60,
],
]
]);
$reminderList = $entityManager
->getRDBRepositoryByClass(Reminder::class)
->where([
'entityId' => $meeting->getId(),
'entityType' => $meeting->getEntityType(),
])
->find();
$this->assertCount(2, $reminderList);
}
}