<?php declare(strict_types=1); namespace App\Command; use App\Entity\Adherent; use App\Entity\Flux; use App\Entity\GlobalParameter; use App\Entity\Prestataire; use App\Entity\SolidoumeItem; use App\Entity\SolidoumeParameter; use App\Entity\TransactionAdherentPrestataire; use App\Entity\TransactionPrestataireAdherent; use App\Entity\User; use App\Enum\CurrencyEnum; use App\Enum\MoyenEnum; use App\Utils\CustomEntityManager; use App\Utils\OperationUtils; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Twig\Environment; /** * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ class SolidoumeCommand extends Command { protected static $defaultName = 'kohinos:solidoume:execute'; protected $em; protected $logger; protected $mailer; protected $templating; protected $io; protected $param; protected $operationUtils; protected $isTest; protected $testExecute; protected $itemsTest; public function __construct( CustomEntityManager $em, LoggerInterface $cronLogger, \Swift_Mailer $mailer, Environment $templating, OperationUtils $operationUtils ) { $this->em = $em; $this->logger = $cronLogger; $this->mailer = $mailer; $this->templating = $templating; $this->operationUtils = $operationUtils; $this->isTest = false; $this->testExecute = false; $this->itemsTest = null; parent::__construct(); } protected function configure() { $this ->setDescription('Sécurité sociale alimentaire : executer les opérations') ->setDefinition([ new InputOption('test', null, InputOption::VALUE_NONE, 'For testing purpose : only print testresult of reminder email and taking money the right day'), new InputOption('execute', null, InputOption::VALUE_NONE, 'For testing purpose : execute program even if it\s not the correct date fo execution !'), ]) ; } /** * Execute program Solidoume. * * @param InputInterface $input * @param OutputInterface $output * * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); $this->param = $this->em->getRepository(SolidoumeParameter::class)->findTheOne(); if (empty($this->param)) { $this->io->error('Sécurité sociale alimentaire non paramétrée !'); return 0; } $this->em->getConnection()->getConfiguration()->setSQLLogger(null); $this->isTest = $input->getOption('test'); $this->testExecute = $input->getOption('execute'); $this->io->title('Start'); $this->executeReminders(); // Envoi d'un quizz uniquement pour les utilisateurs de Solidoume ! if ($this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') == 'Solidoume') { $this->executeQuizz(); } $this->executeTaking(); sleep(1); $this->executeProgram(); $this->io->success('End'); $memoryUsage = memory_get_usage(true) / 1024 / 1024; $this->io->text("Batch finished with memory: ${memoryUsage}M"); return 0; } /** * Execute quizz : send quizz to all participant who have participated since 3 months minimum */ private function executeQuizz() { $this->io->title('START : Envoi du questionnaire'); $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]); foreach ($items as $item) { if ($this->hasToSendQuizz($item)) { $item->setQuizzSended(true); if (!$this->isTest) { $this->em->persist($item); $this->em->flush(); } $this->sendQuizz($item); } } } /** * Execute email reminder : If balance of account if not enough x days before date of paiement => send email. */ private function executeReminders() { $this->io->title('START : Rappel par email'); $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]); foreach ($items as $item) { if ($this->hasToExecuteReminders($item)) { $amount = $item->getAmount(); $accountEmlc = $item->getAdherent()->getAccountWithCurrency(CurrencyEnum::CURRENCY_EMLC); if (null != $accountEmlc) { if ($accountEmlc->getBalance() < $amount) { // L'adherent n'a pas assez d'argent sur son ecompte ! if ($this->isTest) { $this->io->error('Balance : ' . $accountEmlc->getBalance() . ', montant : ' . $amount); } $this->sendReminder($item); } } else { $this->io->error("L'adherent " . $adherent->__toString() . " n'a pas de compte emlc !"); } } } } /** * Send email reminder. * * @param SolidoumeItem $solidoumeItem */ private function sendReminder(SolidoumeItem $solidoumeItem) { $adherent = $solidoumeItem->getAdherent(); $this->io->success("Envoi de l'email de rappel à l'adhérent : " . $adherent->__toString()); $subject = $this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' : Votre compte n\'a pas un solde suffisant !'; $mail = (new \Swift_Message($subject)) ->setFrom($this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::MLC_NOTIF_EMAIL)) ->setTo($adherent->getUser()->getEmail()) ->setBody( $this->templating->render( '@kohinos/email/solidoume/reminder.html.twig', [ 'subject' => $subject, 'item' => $solidoumeItem, ] ), 'text/html' ); if (!$this->isTest) { $this->mailer->send($mail); } } /** * Send Quizz email. * * @param SolidoumeItem $solidoumeItem */ private function sendQuizz(SolidoumeItem $solidoumeItem) { $adherent = $solidoumeItem->getAdherent(); $this->io->success("Envoi du questionnaire à l'adhérent : " . $adherent->__toString()); $subject = 'Vos retours sur ' . $this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' !'; $mail = (new \Swift_Message($subject)) ->setFrom($this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::MLC_NOTIF_EMAIL)) ->setTo($adherent->getUser()->getEmail()) ->setBody( $this->templating->render( '@kohinos/email/solidoume/quizz.html.twig', [ 'subject' => $subject, 'item' => $solidoumeItem, ] ), 'text/html' ); if (!$this->isTest) { $this->mailer->send($mail); } } /** * Take money of participant of solidoume if date of today is same as paiementDate. */ private function executeTaking() { $this->io->title('START : prélèvement du jour'); $nowDay = (new \DateTime('now', new \DateTimeZone('UTC')))->format('d'); $nowMonth = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym')); $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true, 'paiementDate' => $nowDay]); try { $em = $this->em; $isTest = $this->isTest; $io = $this->io; $operationUtils = $this->operationUtils; $callback = function () use ($em, $items, $isTest, $io, $nowDay, $nowMonth, $operationUtils) { foreach ($items as $item) { // Prélèvement des emlc sur chaque compte adhérent participant au programme $amount = $item->getAmount(); $accountEmlc = $item->getAdherent()->getAccountWithCurrency(CurrencyEnum::CURRENCY_EMLC); if (null != $accountEmlc && $item->getPaiementDate() == $nowDay && $item->getLastMonthPayed() != $nowMonth) { if ($accountEmlc->getBalance() < $amount) { $io->error($item->getAdherent()->__toString() . ' : Solde de l\'adherent inférieur au montant à prélever (Solde : ' . $accountEmlc->getBalance() . ' eMLC) !'); $item->setEnabled(false); if (!$isTest) { $em->persist($item); } // @TODO : alerter l'utilisateur ? } else { $flux = new TransactionAdherentPrestataire(); $flux->setExpediteur($item->getAdherent()); $flux->setDestinataire($em->getRepository(Prestataire::class)->getPrestataireSolidoume()); $flux->setMontant($amount); $flux->setData($item->toArray()); $flux->setMoyen(MoyenEnum::MOYEN_EMLC); $now = (new \Datetime('now'))->format('d/m/Y H:i:s'); $flux->setReference($em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' prélèvement en date du ' . $now); $item->setLastMonthPayed($nowMonth); if (!$isTest) { $em->persist($flux); $em->persist($item); // Write operations for this flux ! $operationUtils->executeOperations($flux); $em->flush(); } else { } $io->success("Prélèvement $amount € \nFlux : " . $flux->__toString() . "\nItem : " . $item->__toString()); } } } }; $this->em->transactional($callback); } catch (\Exception $e) { // @TODO : tmp throw $e; } } /** * Execute solidoume repartition of money recolted if date of today is same as executionDate. */ private function executeProgram() { $nowDay = (new \DateTime('now', new \DateTimeZone('UTC')))->format('d'); if (!$this->testExecute && $nowDay != $this->param->getExecutionDate()) { $this->io->warning("Ce n'est pas le jour d'execution du programme (" . $this->param->getExecutionDate() . ')'); } else { $this->io->title('START : Répartition de la somme récoltée'); $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]); $total = 0; $countPerson = 0; $countParticipants = 0; $participants = []; foreach ($items as $item) { if ($this->isItemPayedThisMonth($item)) { $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'adherent_prestataire'); $lastPrelevement = null; foreach ($datas as $data) { if ($data->getCreatedAt()->format('d') <= $this->param->getExecutionDate() && $data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m'))) { $lastPrelevement = $data; } elseif ($data->getCreatedAt()->format('d') > $this->param->getExecutionDate() && $data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m')-1)) { $lastPrelevement = $data; } } if ($lastPrelevement != null) { $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'prestataire_adherent'); $lastPaiement = null; foreach ($datas as $data) { if ($data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m'))) { $lastPaiement = $data; } } if ($lastPaiement == null) { $total += $lastPrelevement->getMontant(); if (!$item->getIsDonation()) { ++$countParticipants; } $participants[] = $item; ++$countPerson; } } } } $totalByParticipant = round((($total / $countParticipants) * ((100 - $this->param->getCommission()) / 100)), 2, PHP_ROUND_HALF_DOWN); $this->io->success('Total de eMLC récolté : ' . $total . ' !'); $this->io->success('Nombre de personnes : ' . $countPerson . ' !'); $this->io->success('Nombre de participants au programme : ' . $countParticipants . ' !'); $this->io->success('Total par participants (sans commission) : ' . ($total / $countParticipants) . ' !'); $this->io->success('Total par participants (avec commission de ' . $this->param->getCommission() . '%) : ' . $totalByParticipant . ' !'); try { $em = $this->em; $isTest = $this->isTest; $io = $this->io; $operationUtils = $this->operationUtils; $callback = function () use ($em, $participants, $isTest, $io, $totalByParticipant, $operationUtils) { foreach ($participants as $item) { if ($this->isItemPayedThisMonth($item) && !$item->getIsDonation()) { // Envoi de l'emlc sur le compte de l'adherent $flux = new TransactionPrestataireAdherent(); $flux->setExpediteur($em->getRepository(Prestataire::class)->getPrestataireSolidoume()); $flux->setDestinataire($item->getAdherent()); $flux->setMontant($totalByParticipant); $flux->setMoyen(MoyenEnum::MOYEN_EMLC); $now = (new \Datetime('now'))->format('d/m/Y H:i:s'); $flux->setReference('Correction ' . $em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' ' . $now); if (!$isTest) { $em->persist($flux); // Write operations for this flux ! $operationUtils->executeOperations($flux); $em->flush(); } $io->success("Paiement solidoume\nFlux : " . $flux->__toString() . "\nItem : " . $item->__toString()); } if (!$item->getIsRecurrent()) { // Disabled all non recurrent paiement $item->setEnabled(false); if (!$isTest) { $em->persist($item); } else { $io->text('Paiement non recurrent donc désactivé ! ' . $item->__toString()); } } } if (!$isTest) { $em->flush(); $em->clear(); } }; $this->em->transactional($callback); } catch (\Exception $e) { $this->io->error($e->getMessage()); } } return null; } private function isItemPayedThisMonth(SolidoumeItem $item): bool { $nowMonth = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym')); $lastMonth = intval((new \DateTime('now -1month', new \DateTimeZone('UTC')))->format('Ym')); if ($item->isEnabled() && null != $item->getLastMonthPayed()) { if ($item->getPaiementDate() <= $this->param->getExecutionDate() && $item->getPaiementDate() >= 1) { if ($item->getLastMonthPayed() >= $nowMonth) { return true; } } else { if ($item->getLastMonthPayed() >= $lastMonth) { return true; } } } return false; } private function hasToSendQuizz(SolidoumeItem $item) { $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'adherent_prestataire'); if (count($datas) >= 3 && !$item->isQuizzSended()) { return true; } return false; } /** * Has to execute reminder by email ? Check date of paiement choose by adherent and number of days before remind. * * @param SolidoumeItem $item * * @return bool */ private function hasToExecuteReminders(SolidoumeItem $item) { $nowDay = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('d')); $reminderDays = $this->param->getReminderDays(); $executionDate = intval($item->getPaiementDate()); if ($nowDay > $executionDate) { $d = new \DateTime('now'); $d->modify('first day of next month'); $nextExecution = $d->format('Ym') . ($executionDate < 10 ? '0' . $executionDate : $executionDate); $reminingDate = (new \DateTime('+' . $reminderDays . ' days', new \DateTimeZone('UTC')))->format('Ymd'); if ($reminingDate == $nextExecution) { return true; } } else { $nextExecution = (new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym') . ($executionDate < 10 ? '0' . $executionDate : $executionDate); $reminingDate = (new \DateTime('+' . $reminderDays . ' days', new \DateTimeZone('UTC')))->format('Ymd'); if ($reminingDate == $nextExecution) { return true; } } return false; } }