feat: financial data check for teamers, improved enforced checks logic
addresses #869at5rtp
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Teamer\Check;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\User;
|
||||
use App\Form\FinancialDataType;
|
||||
use App\Model\FinancialDataDto;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class FinancialDataController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/teamer/check/financial-data', name: 'app_teamer_check_financial_data')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
if (null === $teamer) {
|
||||
$teamer = new Teamer();
|
||||
$user->setTeamer($teamer);
|
||||
$this->entityManager->persist($teamer);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$formData = FinancialDataDto::fromTeamer($teamer);
|
||||
$form = $this->createForm(FinancialDataType::class, $formData);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (true === $form->isSubmitted() && true === $form->isValid()) {
|
||||
$formData->applyTo($teamer);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Deine Angaben wurden gespeichert.');
|
||||
$this->logger->info('Submit teamer financial data', [
|
||||
'teamer_id' => $teamer->getId(),
|
||||
'teamer_name' => (string) $teamer,
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_teamer_index');
|
||||
}
|
||||
|
||||
return $this->render('teamer/check/financial_data.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'teamer' => $teamer,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Teamer\Check;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\User;
|
||||
use App\Form\PersonalDataConfirmationType;
|
||||
use App\Model\PersonalDataConfirmationDto;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class PersonalDataController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/teamer/check/personal-data', name: 'app_teamer_check_personal_data')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
if (null === $teamer) {
|
||||
$teamer = new Teamer();
|
||||
$user->setTeamer($teamer);
|
||||
$this->entityManager->persist($teamer);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$form = $this->createForm(PersonalDataConfirmationType::class, new PersonalDataConfirmationDto());
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (true === $form->isSubmitted() && true === $form->isValid()) {
|
||||
$teamer->setDataVerifiedAt(new \DateTimeImmutable());
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Vielen Dank, deine Daten wurden bestätigt.');
|
||||
$this->logger->info('Confirm teamer personal data', [
|
||||
'teamer_id' => $teamer->getId(),
|
||||
'teamer_name' => (string) $teamer,
|
||||
]);
|
||||
|
||||
return $this->redirectToRoute('app_teamer_index');
|
||||
}
|
||||
|
||||
return $this->render('teamer/check/personal_data.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'teamer' => $teamer,
|
||||
// surfaced as a hint only, deliberately not blocking the confirmation
|
||||
'errors' => $this->validator->validate($teamer, null, ['profile_preflight']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use App\Entity\Upload;
|
||||
use App\Entity\User;
|
||||
use App\Form\TeamerProfileType;
|
||||
use App\Model\UploadSessionDto;
|
||||
use App\RequiredTeamerCheck\PersonalDataVerificationRequiredCheck;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -28,7 +27,6 @@ class IndexController extends AbstractController
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly UploadHandler $uploadHandler,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly PersonalDataVerificationRequiredCheck $personalDataVerificationRequiredCheck,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -55,12 +53,8 @@ class IndexController extends AbstractController
|
||||
// Validate teamer data to show missing data right away
|
||||
$errors = $this->validator->validate($teamer, null, ['profile_preflight']);
|
||||
|
||||
$verificationMode = true === $this->personalDataVerificationRequiredCheck->appliesTo($user)
|
||||
&& false === $this->personalDataVerificationRequiredCheck->isSatisfied($user);
|
||||
|
||||
$form = $this->createForm(TeamerProfileType::class, $teamer, [
|
||||
'upload_session' => $uploadSession,
|
||||
'verification_mode' => $verificationMode,
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -72,9 +66,9 @@ class IndexController extends AbstractController
|
||||
} catch (ApiClientException $e) {
|
||||
}
|
||||
|
||||
if (true === $verificationMode) {
|
||||
$teamer->setDataVerifiedAt(new \DateTimeImmutable());
|
||||
}
|
||||
// Saving the full profile form is a stronger statement than ticking the
|
||||
// confirmation box, so it clears the verification check too.
|
||||
$teamer->setDataVerifiedAt(new \DateTimeImmutable());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -91,7 +85,6 @@ class IndexController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'teamer' => $teamer,
|
||||
'errors' => $errors,
|
||||
'verification_mode' => $verificationMode,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ class RequiredTeamerCheckSubscriber implements EventSubscriberInterface
|
||||
|
||||
if (true === in_array($route, [
|
||||
$check->getRouteName(),
|
||||
// the profile is where data gets corrected, so it has to stay reachable
|
||||
// while a check is outstanding - otherwise a teamer asked to confirm
|
||||
// wrong data has no way to fix it first
|
||||
'app_teamer_profile_index',
|
||||
'app_upload_delete',
|
||||
'app_security_logout',
|
||||
], true)) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Model\FinancialDataDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class FinancialDataType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('taxId', TextType::class, [
|
||||
'label' => 'Steuer-ID',
|
||||
])
|
||||
->add('iban', TextType::class, [
|
||||
'label' => 'IBAN',
|
||||
])
|
||||
->add('bic', TextType::class, [
|
||||
'label' => 'BIC',
|
||||
])
|
||||
->add('bank', TextType::class, [
|
||||
'label' => 'Name der Bank',
|
||||
])
|
||||
->add('holder', TextType::class, [
|
||||
'label' => 'Kontoinhaber',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => FinancialDataDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Model\PersonalDataConfirmationDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PersonalDataConfirmationType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('confirmed', CheckboxType::class, [
|
||||
'label' => 'Ich bestätige, dass meine Daten korrekt und aktuell sind',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => PersonalDataConfirmationDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ use App\Model\UploadSessionDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -15,7 +14,6 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||
|
||||
class TeamerProfileType extends AbstractType
|
||||
{
|
||||
@@ -96,19 +94,6 @@ class TeamerProfileType extends AbstractType
|
||||
],
|
||||
])
|
||||
;
|
||||
|
||||
if (true === $options['verification_mode']) {
|
||||
$builder->add('confirmDataVerification', CheckboxType::class, [
|
||||
'label' => 'Ich bestätige, dass meine Daten korrekt und aktuell sind',
|
||||
'mapped' => false,
|
||||
'constraints' => [
|
||||
new IsTrue([
|
||||
'message' => 'Deine Bestätigung ist erforderlich',
|
||||
'groups' => ['profile'],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
@@ -128,11 +113,9 @@ class TeamerProfileType extends AbstractType
|
||||
'validation_groups' => [
|
||||
'profile',
|
||||
],
|
||||
'verification_mode' => false,
|
||||
])
|
||||
->setRequired(['upload_session'])
|
||||
->setAllowedTypes('upload_session', UploadSessionDto::class)
|
||||
->setAllowedTypes('verification_mode', 'bool')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Embeddable\BankAccount;
|
||||
use App\Entity\Teamer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class FinancialDataDto
|
||||
{
|
||||
#[Assert\NotBlank(message: 'Bitte gib deine SteuerID an')]
|
||||
private ?string $taxId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte gib die IBAN an')]
|
||||
#[Assert\Iban(message: 'Bitte gib eine gültige IBAN an')]
|
||||
private ?string $iban = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte gib den BIC an')]
|
||||
private ?string $bic = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte gib den Namen deiner Bank an')]
|
||||
private ?string $bank = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte gib den Kontoinhaber an')]
|
||||
private ?string $holder = null;
|
||||
|
||||
public static function fromTeamer(Teamer $teamer): self
|
||||
{
|
||||
$instance = (new self())->setTaxId($teamer->getTaxId());
|
||||
|
||||
if (null === $bankAccount = $teamer->getBankAccount()) {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
return $instance
|
||||
->setIban($bankAccount->getIban())
|
||||
->setBic($bankAccount->getBic())
|
||||
->setBank($bankAccount->getBank())
|
||||
->setHolder($bankAccount->getHolder())
|
||||
;
|
||||
}
|
||||
|
||||
public function applyTo(Teamer $teamer): void
|
||||
{
|
||||
if (null === $bankAccount = $teamer->getBankAccount()) {
|
||||
$bankAccount = new BankAccount();
|
||||
$teamer->setBankAccount($bankAccount);
|
||||
}
|
||||
|
||||
$bankAccount
|
||||
->setIban($this->getIban())
|
||||
->setBic($this->getBic())
|
||||
->setBank($this->getBank())
|
||||
->setHolder($this->getHolder())
|
||||
;
|
||||
|
||||
$teamer->setTaxId($this->getTaxId());
|
||||
}
|
||||
|
||||
public function getTaxId(): ?string
|
||||
{
|
||||
return $this->taxId;
|
||||
}
|
||||
|
||||
public function setTaxId(?string $taxId): static
|
||||
{
|
||||
$this->taxId = $taxId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIban(): ?string
|
||||
{
|
||||
return $this->iban;
|
||||
}
|
||||
|
||||
public function setIban(?string $iban): static
|
||||
{
|
||||
$this->iban = $iban;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBic(): ?string
|
||||
{
|
||||
return $this->bic;
|
||||
}
|
||||
|
||||
public function setBic(?string $bic): static
|
||||
{
|
||||
$this->bic = $bic;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBank(): ?string
|
||||
{
|
||||
return $this->bank;
|
||||
}
|
||||
|
||||
public function setBank(?string $bank): static
|
||||
{
|
||||
$this->bank = $bank;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHolder(): ?string
|
||||
{
|
||||
return $this->holder;
|
||||
}
|
||||
|
||||
public function setHolder(?string $holder): static
|
||||
{
|
||||
$this->holder = $holder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class PersonalDataConfirmationDto
|
||||
{
|
||||
#[Assert\IsTrue(message: 'Deine Bestätigung ist erforderlich')]
|
||||
private ?bool $confirmed = false;
|
||||
|
||||
public function getConfirmed(): ?bool
|
||||
{
|
||||
return $this->confirmed;
|
||||
}
|
||||
|
||||
public function setConfirmed(?bool $confirmed): static
|
||||
{
|
||||
$this->confirmed = $confirmed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\RequiredTeamerCheck;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
class FinancialDataRequiredCheck extends AbstractRequiredTeamerCheck
|
||||
{
|
||||
public function getCode(): string
|
||||
{
|
||||
return 'financial_data';
|
||||
}
|
||||
|
||||
public function isSatisfied(User $user): bool
|
||||
{
|
||||
$teamer = $user->getTeamer();
|
||||
|
||||
if (null === $teamer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === empty($teamer->getTaxId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bankAccount = $teamer->getBankAccount();
|
||||
|
||||
if (null === $bankAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === empty($bankAccount->getIban())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === empty($bankAccount->getBic())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === empty($bankAccount->getBank())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === empty($bankAccount->getHolder())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRouteName(): string
|
||||
{
|
||||
return 'app_teamer_check_financial_data';
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use Carbon\CarbonImmutable;
|
||||
class PersonalDataVerificationRequiredCheck extends AbstractRequiredTeamerCheck
|
||||
{
|
||||
public function __construct(
|
||||
private readonly array $dataVerificationDeadlines = ['04-01', '10-01'],
|
||||
private readonly RecurringDeadlines $deadlines,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -26,84 +26,28 @@ class PersonalDataVerificationRequiredCheck extends AbstractRequiredTeamerCheck
|
||||
return false;
|
||||
}
|
||||
|
||||
return false === $this->isVerificationRequired($teamer);
|
||||
return false === $this->deadlines->isDue(
|
||||
$teamer->getDataVerifiedAt(),
|
||||
$this->getRegisteredAt($teamer),
|
||||
CarbonImmutable::now(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRouteName(): string
|
||||
{
|
||||
return 'app_teamer_profile_index';
|
||||
}
|
||||
|
||||
private function isVerificationRequired(Teamer $teamer): bool
|
||||
{
|
||||
$now = CarbonImmutable::now()->startOfDay();
|
||||
$deadline = $this->getCurrentDeadline($now);
|
||||
|
||||
if (null === $deadline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->isRegisteredAfterAnyCurrentYearDeadline($teamer, $now)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dataVerifiedAt = $teamer->getDataVerifiedAt();
|
||||
|
||||
if (null === $dataVerifiedAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CarbonImmutable::instance($dataVerifiedAt) < $deadline;
|
||||
}
|
||||
|
||||
private function isRegisteredAfterAnyCurrentYearDeadline(Teamer $teamer, CarbonImmutable $now): bool
|
||||
{
|
||||
try {
|
||||
$registeredAt = CarbonImmutable::instance($teamer->getCreatedAt())->startOfDay();
|
||||
} catch (\TypeError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->getDeadlinesForYear($now) as $deadline) {
|
||||
if ($registeredAt > $deadline) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return 'app_teamer_check_personal_data';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CarbonImmutable[]
|
||||
* Teamer::getCreatedAt() is declared non-nullable but backed by a nullable
|
||||
* column, so it throws for an unpersisted teamer.
|
||||
*/
|
||||
private function getDeadlinesForYear(CarbonImmutable $now): array
|
||||
private function getRegisteredAt(Teamer $teamer): ?\DateTimeImmutable
|
||||
{
|
||||
$deadlines = [];
|
||||
|
||||
foreach (array_filter(array_map('trim', $this->dataVerificationDeadlines)) as $date) {
|
||||
$deadline = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s', $now->format('Y'), $date));
|
||||
|
||||
if (false === $deadline instanceof CarbonImmutable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deadlines[] = $deadline->startOfDay();
|
||||
try {
|
||||
return $teamer->getCreatedAt();
|
||||
} catch (\TypeError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $deadlines;
|
||||
}
|
||||
|
||||
private function getCurrentDeadline(CarbonImmutable $now): ?CarbonImmutable
|
||||
{
|
||||
$deadlines = $this->getDeadlinesForYear($now);
|
||||
rsort($deadlines);
|
||||
|
||||
foreach ($deadlines as $deadline) {
|
||||
if ($deadline <= $now) {
|
||||
return $deadline;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\RequiredTeamerCheck;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
/**
|
||||
* A set of yearly recurring dates after which something must be confirmed again.
|
||||
*
|
||||
* Belongs to the individual check that composes it, never to the application as a
|
||||
* whole: a deadline expresses "this particular confirmation has expired", not
|
||||
* "portal access has expired".
|
||||
*/
|
||||
final class RecurringDeadlines
|
||||
{
|
||||
/**
|
||||
* @param string[] $deadlines dates as MM-DD, e.g. ['04-01', '10-01']; an empty
|
||||
* list is never due
|
||||
*/
|
||||
public function __construct(private readonly array $deadlines)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a confirmation is outstanding: never confirmed, or last confirmed
|
||||
* before the most recent deadline that has passed.
|
||||
*/
|
||||
public function isDue(?\DateTimeInterface $confirmedAt, ?\DateTimeInterface $registeredAt, CarbonImmutable $now): bool
|
||||
{
|
||||
$deadline = $this->getCurrentDeadline($now);
|
||||
|
||||
if (null === $deadline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->isRegisteredAfterAnyDeadline($registeredAt, $now)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $confirmedAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CarbonImmutable::instance($confirmedAt) < $deadline;
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent deadline that has already passed, or null when none has.
|
||||
*/
|
||||
public function getCurrentDeadline(CarbonImmutable $now): ?CarbonImmutable
|
||||
{
|
||||
$deadlines = $this->getDeadlinesForYear($now);
|
||||
rsort($deadlines);
|
||||
|
||||
foreach ($deadlines as $deadline) {
|
||||
if ($deadline <= $now) {
|
||||
return $deadline;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Someone who registered after a deadline has effectively confirmed their data
|
||||
* by registering, so they are exempt until the next one.
|
||||
*/
|
||||
private function isRegisteredAfterAnyDeadline(?\DateTimeInterface $registeredAt, CarbonImmutable $now): bool
|
||||
{
|
||||
if (null === $registeredAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$registeredAt = CarbonImmutable::instance($registeredAt)->startOfDay();
|
||||
|
||||
foreach ($this->getDeadlinesForYear($now) as $deadline) {
|
||||
if ($registeredAt > $deadline) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CarbonImmutable[]
|
||||
*/
|
||||
private function getDeadlinesForYear(CarbonImmutable $now): array
|
||||
{
|
||||
$deadlines = [];
|
||||
|
||||
foreach (array_filter(array_map('trim', $this->deadlines)) as $date) {
|
||||
$deadline = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s', $now->format('Y'), $date));
|
||||
|
||||
if (false === $deadline instanceof CarbonImmutable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deadlines[] = $deadline->startOfDay();
|
||||
}
|
||||
|
||||
return $deadlines;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user