Commit d73ae436 by Damien Moulard

Merge branch '8686-store-allocation-data' into 'develop'

create allocation entity and save it when saving allocation flux

See merge request !154
parents 1d7dc9dc 7bc57959
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Adherent;
use App\Entity\Flux;
use App\Entity\Allocation;
use App\Utils\CustomEntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\InputOption;
/**
* This command is used to create Allocation instances for existing Flux,
* in order to be used in an external context
*/
class CreateAllocationsFromExistingFlux extends Command
{
protected static $defaultName = 'kohinos:ssa:create-allocations-from-existing-flux';
protected $em;
protected $io;
protected $param;
public function __construct(CustomEntityManager $em) {
$this->em = $em;
parent::__construct();
}
protected function configure()
{
$this
->setDescription('SSA : créer les instances d\'allocations à partir des flux de cotisation existantes')
->addOption(
'batchamount',
null,
InputOption::VALUE_REQUIRED,
"Nombre de cotisations à traiter lors de cette opération"
);
;
}
private function readCsv() {
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
$this->io->title('START - creating allocations');
// get batch amount param
$batchAmount = $input->hasOption('batchamount') ? $input->getOption('batchamount') : null;
// get cotisations that don't have an allocation set
$fluxRepository = $this->em->getRepository(Flux::class);
$cotisations = $fluxRepository->getTavCotisationsWithoutAllocation($batchAmount);
$processedComplementaryFluxIds = [];
$count = 0;
foreach ($cotisations as $key => $cotis) {
// $this->io->success(json_encode($cotis));
// create allocation
$allocation = new Allocation();
$allocation->setCotisationFlux($fluxRepository->find($cotis["id"]));
// set adherent
if ($cotis["adherent_id"] !== null) {
$adherent_id = $cotis["adherent_id"];
} else {
$adherent_id = $cotis["adherent_dest_id"];
}
$allocation->setAdherent($this->em->getRepository(Adherent::class)->find($adherent_id));
$amount = floatval($cotis["montant"]);
/**
* An error can have occured with online payment leading to a payment being processed twice.
* In this case, two flux with a complementary flux for each of them have been created at the same time.
* Then, getRelatedTavAllocationFlux() will return multiple results, so we need not to process the same flux twice.
*
* So we look for a complementary flux that exists and that's not been processed before
*/
$complementaryFlux = null;
$possibleComplementaryFlux = $fluxRepository->getRelatedTavAllocationFlux($cotis["id"]);
foreach($possibleComplementaryFlux as $f) {
if ($f["id"] !== null && array_search($f["id"], $processedComplementaryFluxIds) === false) {
$complementaryFlux = $f;
break;
}
}
if ($complementaryFlux !== null) {
$allocation->setComplementaryFlux($fluxRepository->find($complementaryFlux["id"]));
// mark as processed
$processedComplementaryFluxIds[] = $complementaryFlux["id"];
// if there is a complementary flux, add or subsctract amount to get full allocation amount
if ($complementaryFlux["type"] == "reversement_cotisation_adherent") {
$amount += floatval($complementaryFlux["montant"]);
} else if ($complementaryFlux["type"] == "prelevement_cotisation_adherent") {
$amount -= floatval($complementaryFlux["montant"]);
}
}
$allocation->setAmount($amount);
$this->em->persist($allocation);
$count += 1;
}
$this->em->flush();
$this->io->success("End, created {$count} allocations");
$memoryUsage = memory_get_usage(true) / 1024 / 1024;
$this->io->text("Batch finished with memory: ${memoryUsage}M");
return 0;
}
}
...@@ -181,11 +181,17 @@ class Adherent extends AccountableObject implements AccountableInterface ...@@ -181,11 +181,17 @@ class Adherent extends AccountableObject implements AccountableInterface
*/ */
private $thirdPartyFinancer; private $thirdPartyFinancer;
/**
* @ORM\OneToMany(targetEntity=Allocation::class, mappedBy="adherent", orphanRemoval=true)
*/
private $allocations;
public function __construct() public function __construct()
{ {
$this->accounts = new ArrayCollection(); $this->accounts = new ArrayCollection();
$this->dependentChildren = new ArrayCollection(); $this->dependentChildren = new ArrayCollection();
$this->externalDatedDataCollection = new ArrayCollection(); $this->externalDatedDataCollection = new ArrayCollection();
$this->allocations = new ArrayCollection();
} }
public function getId() public function getId()
...@@ -593,4 +599,34 @@ class Adherent extends AccountableObject implements AccountableInterface ...@@ -593,4 +599,34 @@ class Adherent extends AccountableObject implements AccountableInterface
return $this; return $this;
} }
/**
* @return Collection<int, Allocation>
*/
public function getAllocations(): Collection
{
return $this->allocations;
}
public function addAllocation(Allocation $allocation): self
{
if (!$this->allocations->contains($allocation)) {
$this->allocations[] = $allocation;
$allocation->setAdherent($this);
}
return $this;
}
public function removeAllocation(Allocation $allocation): self
{
if ($this->allocations->removeElement($allocation)) {
// set the owning side to null (unless already changed)
if ($allocation->getAdherent() === $this) {
$allocation->setAdherent(null);
}
}
return $this;
}
} }
<?php
namespace App\Entity;
use App\Repository\AllocationRepository;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=AllocationRepository::class)
*
* In SSA environment, defines the allocation an Adhérent gets when they pay their cotisation.
* An allocation is composed of a cotisation, which is the amount they will pay,
* and an optional complementary flux to add to or withdraw from the user eMLC account so they get an allocation of [amount] eMLC.
*
* This table is primarily used in an external context (i.e. Metabase), to simplify access to allocation data.
*/
class Allocation
{
use TimestampableEntity;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=adherent::class, inversedBy="allocations")
* @ORM\JoinColumn(nullable=false)
*/
private $adherent;
/**
* @ORM\OneToOne(targetEntity=Flux::class, inversedBy="allocation", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
private $cotisationFlux;
/**
* @ORM\OneToOne(targetEntity=Flux::class, inversedBy="allocation", cascade={"persist", "remove"})
*
* Can be null if the user gets the same amount of allocation as their cotisation.
*/
private $complementaryFlux;
/**
* @ORM\Column(type="float")
*
* Amount of the allocation, combinaison of cotisation flux to which is added or added the complementary flux
*/
private $amount;
public function getId(): ?int
{
return $this->id;
}
public function getAdherent(): ?adherent
{
return $this->adherent;
}
public function setAdherent(?adherent $adherent): self
{
$this->adherent = $adherent;
return $this;
}
public function getCotisationFlux(): ?Flux
{
return $this->cotisationFlux;
}
public function setCotisationFlux(Flux $cotisationFlux): self
{
$this->cotisationFlux = $cotisationFlux;
return $this;
}
public function getComplementaryFlux(): ?Flux
{
return $this->complementaryFlux;
}
public function setComplementaryFlux(?Flux $complementaryFlux): self
{
$this->complementaryFlux = $complementaryFlux;
return $this;
}
public function getAmount(): ?float
{
return $this->amount;
}
public function setAmount(float $amount): self
{
$this->amount = $amount;
return $this;
}
}
...@@ -241,6 +241,11 @@ abstract class Flux implements FluxInterface ...@@ -241,6 +241,11 @@ abstract class Flux implements FluxInterface
*/ */
protected $historical; protected $historical;
/**
* @ORM\OneToOne(targetEntity=Allocation::class, mappedBy="cotisationFlux")
*/
private $allocation;
abstract public function getParenttype(): string; abstract public function getParenttype(): string;
abstract public function getType(): string; abstract public function getType(): string;
...@@ -678,4 +683,29 @@ abstract class Flux implements FluxInterface ...@@ -678,4 +683,29 @@ abstract class Flux implements FluxInterface
Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE => Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE => Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE
]; ];
} }
public function getAllocation(): ?Allocation
{
return $this->allocation;
}
public function setAllocation(Allocation $allocation): self
{
// set the other side of the relation if necessary
if (
($this->type == 'vente_emlc_adherent' || $this->type == 'achat_monnaie_adherent')
&& $allocation->getCotisationFlux() !== $this
) {
$allocation->setCotisationFlux($this);
} else if (
($this->type == 'prelevement_cotisation_adherent' || $this->type == 'reversement_cotisation_adherent')
&& $allocation->complementaryFlux() !== $this
) {
$allocation->complementaryFlux($this);
}
$this->allocation = $allocation;
return $this;
}
} }
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260121105005 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE allocation (id INT AUTO_INCREMENT NOT NULL, adherent_id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', cotisation_flux_id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', complementary_flux_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', amount DOUBLE PRECISION NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_5C44232A25F06C53 (adherent_id), UNIQUE INDEX UNIQ_5C44232A363D74 (cotisation_flux_id), UNIQUE INDEX UNIQ_5C44232A7D9C219F (complementary_flux_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_general_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE allocation ADD CONSTRAINT FK_5C44232A25F06C53 FOREIGN KEY (adherent_id) REFERENCES adherent (id)');
$this->addSql('ALTER TABLE allocation ADD CONSTRAINT FK_5C44232A363D74 FOREIGN KEY (cotisation_flux_id) REFERENCES flux (id)');
$this->addSql('ALTER TABLE allocation ADD CONSTRAINT FK_5C44232A7D9C219F FOREIGN KEY (complementary_flux_id) REFERENCES flux (id)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE allocation');
}
}
<?php
namespace App\Repository;
use App\Entity\Allocation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Allocation>
*
* @method Allocation|null find($id, $lockMode = null, $lockVersion = null)
* @method Allocation|null findOneBy(array $criteria, array $orderBy = null)
* @method Allocation[] findAll()
* @method Allocation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AllocationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Allocation::class);
}
/**
* @throws ORMException
* @throws OptimisticLockException
*/
public function add(Allocation $entity, bool $flush = true): void
{
$this->_em->persist($entity);
if ($flush) {
$this->_em->flush();
}
}
/**
* @throws ORMException
* @throws OptimisticLockException
*/
public function remove(Allocation $entity, bool $flush = true): void
{
$this->_em->remove($entity);
if ($flush) {
$this->_em->flush();
}
}
// /**
// * @return Allocation[] Returns an array of Allocation objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->orderBy('a.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Allocation
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
...@@ -452,4 +452,62 @@ class FluxRepository extends ServiceEntityRepository ...@@ -452,4 +452,62 @@ class FluxRepository extends ServiceEntityRepository
return $results; return $results;
} }
/**
* @param int $limit, amount to fetch
*
* @return Array list of cotisations
*/
public function getTavCotisationsWithoutAllocation($limit)
{
$sqlQuery = "SELECT f.id, f.montant, f.adherent_id, f.adherent_dest_id
FROM {$this->tableName} f
LEFT JOIN allocation a ON a.cotisation_flux_id = f.id
WHERE f.type IN ('achat_monnaie_adherent', 'vente_emlc_adherent')
AND a.id IS NULL
ORDER BY f.created_at DESC";
if (null !== $limit && 0 !== intval($limit)) {
$sqlQuery .= " LIMIT " . intval($limit);
}
$statement = $this->connection->prepare($sqlQuery);
$statement->execute();
$results = $statement->fetchAll();
return $results;
}
/**
* Given a TAV cotisation flux, get the potential complementary flux that completes the full TAV allocation.
* Get a flux that's not been already saved as a complementary flux in allocation.
*
* @see App\Entity\Allocation
*
* @param int $fluxCotisId, cotisation flux id
*/
public function getRelatedTavAllocationFlux($fluxCotisId) {
$sqlQuery = "SELECT fb.id, fb.type, fb.montant
FROM {$this->tableName} f
LEFT JOIN flux fb
ON ABS(TIMESTAMPDIFF(MICROSECOND, f.created_at, fb.created_at)) < 2000000
AND
(
f.adherent_id = fb.adherent_id
OR f.adherent_id = fb.adherent_dest_id
OR f.adherent_dest_id = fb.adherent_id
OR f.adherent_dest_id = fb.adherent_dest_id
)
AND fb.type IN ('prelevement_cotisation_adherent', 'reversement_cotisation_adherent')
LEFT JOIN allocation a ON a.complementary_flux_id = fb.id
WHERE f.id = :fluxId
AND a.id IS NULL";
$statement = $this->connection->prepare($sqlQuery);
$statement->bindValue(':fluxId', $fluxCotisId);
$statement->execute();
$results = $statement->fetchAll();
return $results;
}
} }
...@@ -13,6 +13,7 @@ use App\Entity\Flux; ...@@ -13,6 +13,7 @@ use App\Entity\Flux;
use App\Entity\CotisationTavReversement; use App\Entity\CotisationTavReversement;
use App\Entity\CotisationTavPrelevement; use App\Entity\CotisationTavPrelevement;
use App\Entity\ThirdPartyAllocationFunding; use App\Entity\ThirdPartyAllocationFunding;
use App\Entity\Allocation;
use App\Enum\MoyenEnum; use App\Enum\MoyenEnum;
use App\Repository\PaymentRepository; use App\Repository\PaymentRepository;
use App\Utils\CustomEntityManager; use App\Utils\CustomEntityManager;
...@@ -133,11 +134,17 @@ class TAVCotisationUtils ...@@ -133,11 +134,17 @@ class TAVCotisationUtils
$profile = $adherent->getProfilDeCotisation(); $profile = $adherent->getProfilDeCotisation();
$cotisationTaux = $profile->getTauxCotisation(); $cotisationTaux = $profile->getTauxCotisation();
// create the Allocation
$allocation = new Allocation();
$allocation->setAdherent($adherent);
$allocation->setCotisationFlux($flux);
// don't need to create an other Flux if the rate is 1 // don't need to create an other Flux if the rate is 1
if ($cotisationTaux != 1) { if ($cotisationTaux != 1) {
// calculate the mlc amount the user will receive // calculate the mlc amount the user will receive
$cotisationAmount = $profile->getMontant(); $cotisationAmount = $profile->getMontant();
$mlcAmount = round($cotisationAmount * $cotisationTaux); $mlcAmount = round($cotisationAmount * $cotisationTaux);
$allocation->setAmount($mlcAmount);
// get the difference between what the user paid and what he•she's supposed to receive // get the difference between what the user paid and what he•she's supposed to receive
$amountDiff = $mlcAmount - $cotisationAmount; $amountDiff = $mlcAmount - $cotisationAmount;
...@@ -175,7 +182,16 @@ class TAVCotisationUtils ...@@ -175,7 +182,16 @@ class TAVCotisationUtils
$fluxCotis->setRole($flux->getRole()); $fluxCotis->setRole($flux->getRole());
$fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC); $fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC);
$this->em->persist($fluxCotis); $this->em->persist($fluxCotis);
$allocation->setComplementaryFlux($fluxCotis);
$this->em->persist($allocation);
$this->operationUtils->executeOperations($fluxCotis); $this->operationUtils->executeOperations($fluxCotis);
} else {
$cotisationAmount = $profile->getMontant();
$allocation->setAmount($cotisationAmount);
$this->em->persist($allocation);
$this->em->flush();
} }
} }
...@@ -317,6 +333,7 @@ class TAVCotisationUtils ...@@ -317,6 +333,7 @@ class TAVCotisationUtils
/** /**
* Method called to create Flux based on allowance amount (for household based allowance). * Method called to create Flux based on allowance amount (for household based allowance).
* Only create flux if amount paid != allowance amount. * Only create flux if amount paid != allowance amount.
* Also creates an Allocation instance.
* Also creates a ThirdPartyAllocationFunding instance if needed. * Also creates a ThirdPartyAllocationFunding instance if needed.
*/ */
public function applyHouseholdAllowance(Flux $flux) { public function applyHouseholdAllowance(Flux $flux) {
...@@ -338,6 +355,12 @@ class TAVCotisationUtils ...@@ -338,6 +355,12 @@ class TAVCotisationUtils
$mlcAllowanceAmount = $adherent->getAllocationAmount(); $mlcAllowanceAmount = $adherent->getAllocationAmount();
} }
// create the Allocation
$allocation = new Allocation();
$allocation->setAdherent($adherent);
$allocation->setCotisationFlux($flux);
$allocation->setAmount($mlcAllowanceAmount);
// get the difference between what the user paid and what he•she's supposed to receive // get the difference between what the user paid and what he•she's supposed to receive
$amountDiff = $mlcAllowanceAmount - $cotisationAmount; $amountDiff = $mlcAllowanceAmount - $cotisationAmount;
...@@ -375,7 +398,14 @@ class TAVCotisationUtils ...@@ -375,7 +398,14 @@ class TAVCotisationUtils
$fluxCotis->setRole($flux->getRole()); $fluxCotis->setRole($flux->getRole());
$fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC); $fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC);
$this->em->persist($fluxCotis); $this->em->persist($fluxCotis);
$allocation->setComplementaryFlux($fluxCotis);
$this->em->persist($allocation);
$this->operationUtils->executeOperations($fluxCotis); $this->operationUtils->executeOperations($fluxCotis);
} else {
$this->em->persist($allocation);
$this->em->flush();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment