Project 'cooperatic/kohinos-tav' was moved to 'agplv3/kohinos-tav'. Please update any links and bookmarks that may still have the old path.
Commit 1527feab by Yvon Kerdoncuff

Merge branch '5735-allowance-based-on-household' into 'ssa-gironde'

create new method to distribute TAV allowance, based on household

See merge request cooperatic/kohinos-tav!70
parents 3a401fc8 fa204e43
......@@ -111,7 +111,7 @@ legend.required:after {
margin-bottom: 20px;
}
.formEncaisserCotisationAdherent-no-profile {
.formEncaisserCotisationAdherent-no-cotisation-amount {
font-style: italic;
color: #ff4136;
}
......
......@@ -624,20 +624,20 @@ $(function() {
if (cotisationmontant === undefined) {
$("#formEncaisserCotisationAdherent-montant-container").hide();
$("#formEncaisserCotisationAdherent-no-profile").hide();
$("#formEncaisserCotisationAdherent-no-cotisation-amount").hide();
return;
}
if (cotisationmontant !== null) {
$("#formEncaisserCotisationAdherent-montant-display").text(`${cotisationmontant} €`);
$("#formEncaisserCotisationAdherent-montant-container").show();
$("#formEncaisserCotisationAdherent-no-profile").hide();
$("#formEncaisserCotisationAdherent-no-cotisation-amount").hide();
$("#formEncaisserCotisationAdherent_save").prop("disabled",false);
} else {
// no cotisation profile
$("#formEncaisserCotisationAdherent-montant-container").hide();
$("#formEncaisserCotisationAdherent-no-profile").show();
$("#formEncaisserCotisationAdherent-no-cotisation-amount").show();
$("#formEncaisserCotisationAdherent_save").prop("disabled",true);
}
......
......@@ -267,6 +267,7 @@ services:
- [ setUserManager, ['@fos_user.user_manager']]
- [ setSecurity, ['@security.helper']]
- [ setEventDispatcher, ['@event_dispatcher']]
- [ setTavCotisationUtils, ['@app.utils.tav_cotisations']]
admin.all.cotisations:
class: App\Admin\CotisationAdmin
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,10 +3,10 @@
"app": {
"js": [
"/build/runtime.6ad5c9da.js",
"/build/app.6ea8ce63.js"
"/build/app.27091a64.js"
],
"css": [
"/build/app.04bd0e32.css"
"/build/app.5cdc32e6.css"
]
},
"admin": {
......
{
"build/app.css": "/build/app.04bd0e32.css",
"build/app.js": "/build/app.6ea8ce63.js",
"build/app.css": "/build/app.5cdc32e6.css",
"build/app.js": "/build/app.27091a64.js",
"build/admin.css": "/build/admin.4de55830.css",
"build/admin.js": "/build/admin.86a2d986.js",
"build/runtime.js": "/build/runtime.6ad5c9da.js",
......
......@@ -136,8 +136,6 @@ class UserAdherentController extends FluxController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// TODO: set CB payment when the functionality is validated
$flux = $form->getData();
// Look for existing cotisation
......@@ -166,12 +164,13 @@ class UserAdherentController extends FluxController
$flux->setDon(null);
}
// TODO redirect to paiement
// Redirect to payment
return $this->forward('App\Controller\PaymentController::preparePaymentAction', [
'form' => $form,
'type' => Payment::TYPE_PAIEMENT_COTISATION_TAV // TODO
'type' => Payment::TYPE_PAIEMENT_COTISATION_TAV
]);
/* For test purposes, comment redirection and uncomment following part to skip payment */
// $this->em->persist($flux);
// $this->operationUtils->executeOperations($flux);
......
......@@ -215,25 +215,54 @@ class UserComptoirController extends FluxController
}
$destinataire = $flux->getDestinataire();
$profile = $destinataire->getProfilDeCotisation();
if (is_null($profile)) {
$this->addFlash(
'error',
$this->translator->trans('Opération impossible : l\'habitant.e n\'a pas de profil de cotisation associé.')
);
if ($this->getParameter('household_based_allowance') == true) {
/* Process: allowance based on household */
$cotisationAmount = $destinataire->getCotisationAmount();
return $this->redirectToRoute('index');
}
// Verifications
if (is_null($cotisationAmount) || is_null($destinataire->getHouseholdAdultCount())) {
$this->addFlash(
'error',
$this->translator->trans("Opération impossible : le profil de l'habitant.e est incomplet, veuillez le compléter dans l'interface d'administration.")
);
$cotisationAmount = $profile->getMontant(); // Amount in € paid by the user
$flux->setMontant($cotisationAmount);
$this->em->persist($flux);
$this->operationUtils->executeOperations($flux);
return $this->redirectToRoute('index');
}
if (is_null($destinataire->getAllocationAmount())) {
$this->tavCotisationUtils->calculateAllowanceAccordingToHousehold($destinataire);
$this->em->persist($destinataire);
}
$flux->setMontant($cotisationAmount);
$this->em->persist($flux);
$this->operationUtils->executeOperations($flux);
// Apply cotisation rate, create new flux
$this->tavCotisationsUtils->applyHouseholdAllowance($flux);
} else {
/* Process: allowance based on cotisation profile with cotisation rate */
$profile = $destinataire->getProfilDeCotisation();
if (is_null($profile)) {
$this->addFlash(
'error',
$this->translator->trans('Opération impossible : l\'habitant.e n\'a pas de profil de cotisation associé.')
);
return $this->redirectToRoute('index');
}
// Apply cotisation rate, create new flux
$this->tavCotisationsUtils->applyTauxCotisation($flux);
$cotisationAmount = $profile->getMontant(); // Amount in € paid by the user
$flux->setMontant($cotisationAmount);
$this->em->persist($flux);
$this->operationUtils->executeOperations($flux);
// Apply cotisation rate, create new flux
$this->tavCotisationsUtils->applyTauxCotisation($flux);
}
$this->em->flush();
......
......@@ -129,6 +129,21 @@ class Adherent extends AccountableObject implements AccountableInterface
*/
private $householdAdultCount;
/**
* On household based allowance process, define a cotisation amount for each adherent.
*
* @ORM\Column(type="float", nullable=true)
*/
private $cotisationAmount;
/**
* On household based allowance process, the allowance amountis calculated based on household data.
* Calculate and save the allocation amount when the household data is updated.
*
* @ORM\Column(type="float", nullable=true)
*/
private $allocationAmount;
public function __construct()
{
......@@ -407,4 +422,28 @@ class Adherent extends AccountableObject implements AccountableInterface
return $this;
}
public function getCotisationAmount(): ?float
{
return $this->cotisationAmount;
}
public function setCotisationAmount(?float $cotisationAmount): self
{
$this->cotisationAmount = $cotisationAmount;
return $this;
}
public function getAllocationAmount(): ?float
{
return $this->allocationAmount;
}
public function setAllocationAmount(?float $allocationAmount): self
{
$this->allocationAmount = $allocationAmount;
return $this;
}
}
......@@ -7,16 +7,19 @@ use App\Utils\OperationFactory;
use Doctrine\ORM\Mapping as ORM;
/**
* Application du taux de cotisation lors du paiement d'une cotisation (au sens TAV).
* Application du calcul de l'allocation (emlc reçues) lors du paiement d'une cotisation (au sens TAV).
*
* Au paiement d'une cotisation:
* - Un premier Flux est enregistré, correspondant à l'achat/vente d'emlc.
* - Puis on applique le taux défini dans le ProfilDeCotisation de l'adhérent,
* on crée un nouveau flux pour compléter la cositsation.
* - On crée un nouveau flux pour compléter la cotisation, en fonction du système de calcul choisi.
*
* Systèmes de calculs possibles :
* - On applique le taux défini dans le ProfilDeCotisation de l'adhérent
* - On calcule le montant à recevoir en fonction du foyer de l'adhérent
*
* @ORM\Entity
*/
class TauxCotisationApplication extends Flux
class CotisationTavApplication extends Flux
{
const TYPE_REVERSEMENT_COTISATION_ADHERENT = 'reversement_cotisation_adherent';
const TYPE_PRELEVEMENT_COTISATION_ADHERENT = 'prelevement_cotisation_adherent';
......@@ -26,7 +29,7 @@ class TauxCotisationApplication extends Flux
*/
public function getParenttype(): string
{
return parent::TYPE_APPLICATION_TAUX_COTISATION;
return parent::TYPE_APPLICATION_COTISATION_TAV;
}
public function getAllOperations($em)
......
......@@ -7,12 +7,16 @@ use App\Utils\OperationFactory;
use Doctrine\ORM\Mapping as ORM;
/**
* En cas de taux < 1, l'adhérent•e reçoit moins d'emlc que ce qu'elle•il paye en € :
* Dans les cas suivants :
* - [Profil de Cotisation] La taux est inférieur à 1
* - [Allocation selon foyer] Le montant reçu calculé est inférieur au montant payé
*
* L'adhérent•e reçoit moins d'emlc que ce qu'elle•il paye en €,
* un second flux est créé pour prélever le complément de la cotisation.
*
* @ORM\Entity
*/
class TauxCotisationPrelevement extends TauxCotisationApplication
class CotisationTavPrelevement extends CotisationTavApplication
{
/**
* @ORM\OneToOne(targetEntity="Adherent")
......
......@@ -7,12 +7,16 @@ use App\Utils\OperationFactory;
use Doctrine\ORM\Mapping as ORM;
/**
* En cas de taux > 1, l'adhérent•e reçoit plus d'emlc que ce qu'elle•il paye en € :
* * Dans les cas suivants :
* - [Profil de Cotisation] La taux est supérieur à 1
* - [Allocation selon foyer] Le montant reçu calculé est supérieur au montant payé
*
* L'adhérent•e reçoit plus d'emlc que ce qu'elle•il paye en €,
* un second flux est créé pour reverser le complément de la cotisation.
*
* @ORM\Entity
*/
class TauxCotisationReversement extends TauxCotisationApplication
class CotisationTavReversement extends CotisationTavApplication
{
/**
* @ORM\OneToOne(targetEntity="Siege")
......
......@@ -54,9 +54,9 @@ use Symfony\Component\Validator\Constraints as Assert;
* "ticket_fix" = "TicketFix",
* "ticket_fix_print" = "TicketFixPrint",
* "ticket_fix_destroy" = "TicketFixDestroy",
* "application_taux_cotisation" = "TauxCotisationApplication",
* "reversement_cotisation_adherent" = "TauxCotisationReversement",
* "prelevement_cotisation_adherent" = "TauxCotisationPrelevement",
* "application_cotisation_tav" = "CotisationTavApplication",
* "reversement_cotisation_adherent" = "CotisationTavReversement",
* "prelevement_cotisation_adherent" = "CotisationTavPrelevement",
* })
*/
abstract class Flux implements FluxInterface
......@@ -74,7 +74,7 @@ abstract class Flux implements FluxInterface
const TYPE_VENTE = 'vente';
const TYPE_VENTE_EMLC = 'vente_emlc';
const TYPE_TICKET_FIX = 'ticket_fix';
const TYPE_APPLICATION_TAUX_COTISATION = 'application_taux_cotisation';
const TYPE_APPLICATION_COTISATION_TAV = 'application_cotisation_tav';
/**
* @var \Ramsey\Uuid\UuidInterface
......
......@@ -125,7 +125,7 @@ class ProfilDeCotisation
}
/**
* setContacts.
* setBeneficiaires.
*
* @param [type] $beneficiaires [description]
*/
......
......@@ -36,11 +36,22 @@ class AchatMonnaieFormType extends FluxFormType
if ($this->container->getParameter('tav_env')) {
$montant = 0;
$profilDeCotisation = $this->security->getUser()->getAdherent()->getProfilDeCotisation();
if (null != $profilDeCotisation) {
$montant = $profilDeCotisation->getMontant();
if ($this->container->getParameter('household_based_allowance')) {
$cosisationMontant = $this->security->getUser()->getAdherent()->getCotisationAmount();
if (null != $cosisationMontant) {
$montant = $cosisationMontant;
} else {
$montant = false;
}
} else {
$montant = false;
$profilDeCotisation = $this->security->getUser()->getAdherent()->getProfilDeCotisation();
if (null != $profilDeCotisation) {
$montant = $profilDeCotisation->getMontant();
} else {
$montant = false;
}
}
$builder
......
......@@ -20,13 +20,17 @@ class EncaisserCotisationAdherentFormType extends VenteEmlcAdherentFormType
// Get the cotisation amount for each adherent. Used for display purposes on the front end.
$adherents = $this->em->getRepository(Adherent::class)->findOrderByName();
$adherentsProfiles = [];
$adherentsCotisationAmounts = [];
foreach ($adherents as $adh) {
$montant = null;
if (!is_null($adh->getProfilDeCotisation()) ) {
if ($this->container->getParameter('household_based_allowance') && !is_null($adh->getCotisationAmount())) {
$montant = $adh->getCotisationAmount();
} else if (!$this->container->getParameter('household_based_allowance') && !is_null($adh->getProfilDeCotisation())) {
$montant = $adh->getProfilDeCotisation()->getMontant();
}
$adherentsProfiles[strval($adh->getId())] = $montant;
$adherentsCotisationAmounts[strval($adh->getId())] = $montant;
}
$builder
......@@ -37,7 +41,7 @@ class EncaisserCotisationAdherentFormType extends VenteEmlcAdherentFormType
'data' => 0
])
->add('cotisationMontants', HiddenType::class, [
'data' => json_encode($adherentsProfiles),
'data' => json_encode($adherentsCotisationAmounts),
'mapped' => false
])
;
......
<?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 Version20240313125437 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('ALTER TABLE adherent ADD cotisation_amount DOUBLE PRECISION DEFAULT NULL, ADD allocation_amount DOUBLE PRECISION DEFAULT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE adherent DROP cotisation_amount, DROP allocation_amount');
}
}
......@@ -5,8 +5,8 @@ namespace App\Utils;
use App\Entity\Adherent;
use App\Entity\Siege;
use App\Entity\Flux;
use App\Entity\TauxCotisationReversement;
use App\Entity\TauxCotisationPrelevement;
use App\Entity\CotisationTavReversement;
use App\Entity\CotisationTavPrelevement;
use App\Enum\MoyenEnum;
use App\Utils\CustomEntityManager;
use Symfony\Component\Security\Core\Security;
......@@ -45,6 +45,9 @@ class TAVCotisationUtils
}
/**
* First method to calculate allowance:
* according to a contribution rate defined in user's profile (ProfilDeCotisation).
*
* Apply the cotisation profile rate to the amount paid
* and register the complement as a new flux (only if rate != 1)
*
......@@ -72,14 +75,14 @@ class TAVCotisationUtils
if ($amountDiff > 0) {
// User should receive more than he•she paid: send a new flux to the user to complete its cotisation
$fluxCotis = new TauxCotisationReversement();
$fluxCotis = new CotisationTavReversement();
$fluxCotis->setExpediteur($siege);
$fluxCotis->setDestinataire($flux->getDestinataire());
$fluxCotis->setMontant($amountDiff);
$fluxCotis->setReference("Reversement cotisation après paiement de " . $cotisationAmount . "€ et application du taux " . $cotisationTaux);
} else {
// User should receive less than he•she paid: fetch the difference from his account
$fluxCotis = new TauxCotisationPrelevement();
$fluxCotis = new CotisationTavPrelevement();
$fluxCotis->setExpediteur($flux->getDestinataire());
$fluxCotis->setDestinataire($siege);
$fluxCotis->setMontant(-$amountDiff);
......@@ -95,6 +98,93 @@ class TAVCotisationUtils
}
/**
* Second method to calculate allowance:
* allowance based on user's household.
*
* Rules are as follow:
* - 150 emlc for the first person in user's household
* - 75 emlc for each other adult
* - 75 emlc amount for each dependant child, with a percentage applied if the child is in shared custody:
* 25%, 50% or 75% depending on the shared custody arrangement
*
* Once the full amount is calculated, cap user's balance.
* User account balance is capped at twice the amount previously calculated.
*
* @param Adherent $adherent (by ref)
*/
public function calculateAllowanceAccordingToHousehold(&$adherent) {
// TODO base amounts to param in .env, or in global params ?
// base allowance, for one adult
$mlcAllowanceAmount = 150;
$adultsCount = $adherent->getHouseholdAdultCount();
if ($adultsCount == null) {
return;
}
// increment for each other adult in the household
$mlcAllowanceAmount += 75 * ($adultsCount - 1);
// increment allowance for each dependant child, depending on the shared custody arrangement
$dependentChildren = $adherent->getDependentChildren();
foreach ($dependentChildren as $child) {
$childAllowanceAmount = 75;
$sharedCustodyPercentage = $child->getSharedCustodyPercentage();
if ($sharedCustodyPercentage != null) {
$childAllowanceAmount = $childAllowanceAmount * $sharedCustodyPercentage;
}
$mlcAllowanceAmount += $childAllowanceAmount;
}
$adherent->setAllocationAmount($mlcAllowanceAmount);
}
/**
* Method called to create Flux based on allowance amount (for household based allowance).
*/
public function applyHouseholdAllowance(Flux $flux) {
// get allowance
$adherent = $flux->getDestinataire();
$cotisationAmount = $flux->getMontant();
// get the mlc amount the user is supposed to receive
$mlcAllowanceAmount = $adherent->getAllocationAmount();
if ($flux->getExpediteur() instanceof Siege) {
$siege = $flux->getExpediteur();
} else {
$siege = $flux->getExpediteur()->getGroupe()->getSiege();
}
// get the difference between what the user paid and what he•she's supposed to receive
$amountDiff = $mlcAllowanceAmount - $cotisationAmount;
if ($amountDiff > 0) {
// User should receive more than he•she paid: send a new flux to the user to complete its cotisation
$fluxCotis = new CotisationTavReversement();
$fluxCotis->setExpediteur($siege);
$fluxCotis->setDestinataire($adherent);
$fluxCotis->setMontant($amountDiff);
$fluxCotis->setReference("Reversement du complément de cotisation après paiement de " . $cotisationAmount . "€ pour une allocation de " . $mlcAllowanceAmount . " MonA.");
} else {
// User should receive less than he•she paid: fetch the difference from his account
$fluxCotis = new CotisationTavPrelevement();
$fluxCotis->setExpediteur($adherent);
$fluxCotis->setDestinataire($siege);
$fluxCotis->setMontant(-$amountDiff);
$fluxCotis->setReference("Prélèvement du complément de cotisation après paiement de " . $cotisationAmount . "€ pour une allocation de " . $mlcAllowanceAmount . " MonA.");
}
$fluxCotis->setOperateur($flux->getOperateur());
$fluxCotis->setRole($flux->getRole());
$fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC);
$this->em->persist($fluxCotis);
$this->operationUtils->executeOperations($fluxCotis);
}
/**
* Get the last cotisation of an adhérent
*
* @param Adherent $adherent
......
......@@ -6,9 +6,12 @@
{% block blockcontent %}
{% set form = getPayerCotisationTAVForm(app.user) %}
{% if form.montant.vars.value == false %}
{% if form.montant.vars.value == false and not household_based_allowance %}
<p>{{ 'Vous n\'avez pas de profil de cotisation associé, vous ne pouvez donc pas payer de cotisation.'|trans }}</p>
<p>{{ 'Veuillez contacter un administrateur.'|trans }}</p>
<p>{{ 'Veuillez contacter un•e administrateur•rice.'|trans }}</p>
{% elseif form.montant.vars.value == false and household_based_allowance %}
<p>{{ 'Vous n\'avez pas de montant de cotisation renseigné dans votre profil, vous ne pouvez donc pas payer de cotisation.'|trans }}</p>
<p>{{ 'Veuillez contacter un•e administrateur•rice.'|trans }}</p>
{% else %}
<p>
{{ 'Montant de la cotisation à payer'|trans }} : <span class="paiement_cotisation_montant">{{ form.montant.vars.value }}</span>
......
......@@ -9,9 +9,13 @@
<h5> <b>Montant de la cotisation : <span id="formEncaisserCotisationAdherent-montant-display"></span></b></h5>
<br/>
</div>
<div id="formEncaisserCotisationAdherent-no-profile" style="display:none">
<p class="formEncaisserCotisationAdherent-no-profile">
L'habitant•e n'a pas de profil de cotisation affecté, impossible de l'encaisser.
<div id="formEncaisserCotisationAdherent-no-cotisation-amount" style="display:none">
<p class="formEncaisserCotisationAdherent-no-cotisation-amount">
{% if household_based_allowance %}
L'habitant•e n'a pas de montant de cotisation renseigné, impossible de l'encaisser.
{% else %}
L'habitant•e n'a pas de profil de cotisation affecté, impossible de l'encaisser.
{% endif %}
</p>
</div>
{% set form = getComptoirEncaisserCotisationForm(app.user) %}
......
......@@ -89,6 +89,7 @@ des_vente_adherent_email_subject: 'Vente de billets à un adhérent'
des_vente_prestataire_email_subject: 'Vente de billets à un prestataire'
des_vente_emlc_adherent_email_subject: 'Vente de monnaie numérique à un adhérent'
des_vente_emlc_prestataire_email_subject: 'Vente de monnaie numérique à un prestataire'
des_application_cotisation_tav: 'Complément de cotisation'
confirmation-cotisation-title: 'Cotisation bien reçue !'
confirmation-cotisation-content: 'Cotisation bien reçue, merci !'
confirmation-cotisation-footer-cta: 'acheter de la monnaie locale numérique'
......
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