diff --git a/app/config/services.yml b/app/config/services.yml index fd465e8b1..b0cc9f3a4 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -23,6 +23,9 @@ services: CCMBenchmark\Ting\Repository\Repository: tags: [ 'app.vendor.ting_repository' ] + AppBundle\Accounting\Invoices\Generator\InvoiceGeneration: + tags: [ 'app.invoice_generation_handler' ] + Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: public: false arguments: @@ -167,8 +170,12 @@ services: Afup\Site\Forum\Inscriptions: autowire: true - Afup\Site\Association\CotisationsFactory: - autowire: true + AppBundle\Accounting\Invoices\Generator\MemberInvoiceGenerator: ~ + AppBundle\Accounting\Invoices\Generator\CompanyMemberInvoiceGenerator: ~ + + AppBundle\Accounting\Invoices\InvoiceGenerator: + arguments: + $handlers: !tagged_iterator app.invoice_generation_handler Afup\Site\Corporate\Page: autowire: true @@ -176,9 +183,6 @@ services: Afup\Site\Forum\Facturation: autowire: true - Afup\Site\Association\Cotisations: - factory: ['@Afup\Site\Association\CotisationsFactory', 'create'] - Afup\Site\Comptabilite\Comptabilite: autowire: true diff --git a/sources/Afup/Association/Cotisations.php b/sources/Afup/Association/Cotisations.php deleted file mode 100644 index fa2dfa576..000000000 --- a/sources/Afup/Association/Cotisations.php +++ /dev/null @@ -1,705 +0,0 @@ -value; - $requete .= ' AND id_personne=' . $id_personne . ' '; - $requete .= 'ORDER BY ' . $ordre; - if ($associatif) { - return $this->_bdd->obtenirAssociatif($requete); - } else { - return $this->_bdd->obtenirTous($requete); - } - } - - /** - * Renvoit la cotisation demandée - * - * @param int $id Identifiant de la cotisation - * @param string $champs Champs à renvoyer - * @return array - */ - public function obtenir($id, string $champs = '*') - { - $requete = 'SELECT'; - $requete .= ' ' . $champs . ' '; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' id=' . $id; - return $this->_bdd->obtenirEnregistrement($requete); - } - - /** - * Renvoit le numéro de la prochaine facture au format : {année}-{index depuis le début de l'année} - * - */ - public function _genererNumeroFacture(): string - { - $requete = 'SELECT'; - $requete .= " MAX(CAST(SUBSTRING_INDEX(numero_facture, '-', -1) AS UNSIGNED)) + 1 "; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' LEFT(numero_facture, 4)=' . $this->_bdd->echapper(date('Y')); - $requete .= ' OR LEFT(numero_facture, 10)=' . $this->_bdd->echapper("COTIS-" . date('Y')); - $index = $this->_bdd->obtenirUn($requete); - return 'COTIS-' . date('Y') . '-' . (is_null($index) ? 1 : $index); - } - - /** - * Ajoute une cotisation - * - * @param int $id_personne Identifiant de la personne - * @param float $montant Adresse de la personne - * @param int $type_reglement Type de règlement (espèces, chèque, virement) - * @param string $informations_reglement Informations concernant le règlement (numéro de chèque, de virement etc.) - * @param int $date_debut Date de début de la - * cotisation - * @param int $date_fin Date de fin de la cotisation - * @param string $commentaires Commentaires concernnant la cotisation - * @param string $referenceClient Reference client à mentionner sur la facture - * @return bool Succès de l'ajout - */ - public function ajouter(MemberType $type_personne, $id_personne, $montant, $type_reglement, - $informations_reglement, $date_debut, $date_fin, $commentaires, $referenceClient = null): bool - { - $requete = 'INSERT INTO '; - $requete .= ' afup_cotisations (type_personne, id_personne, montant, type_reglement , informations_reglement,'; - $requete .= ' date_debut, date_fin, numero_facture, token, commentaires, reference_client, date_facture) '; - $requete .= 'VALUES ('; - $requete .= $type_personne->value . ','; - $requete .= $id_personne . ','; - $requete .= $montant . ','; - $requete .= $this->_bdd->echapper($type_reglement) . ','; - $requete .= $this->_bdd->echapper($informations_reglement) . ','; - $requete .= $date_debut . ','; - $requete .= $date_fin . ','; - $requete .= $this->_bdd->echapper($this->_genererNumeroFacture()) . ','; - $requete .= $this->_bdd->echapper(base64_encode(random_bytes(30))) . ','; - $requete .= $this->_bdd->echapper($commentaires) . ','; - $requete .= $this->_bdd->echapper($referenceClient) . ','; - $requete .= $this->_bdd->echapper(date('Y-m-d\TH:i:s')) . ')'; - return $this->_bdd->executer($requete) !== false; - } - - /** - * Modifie une cotisation - * - * @param int $id Identifiant de la cotisation à modifier - * @param int $type_personne Type de la personne (morale ou physique) - * @param int $id_personne Identifiant de la personne - * @param float $montant Adresse de la personne - * @param int $type_reglement Type de règlement (espèces, chèque, virement) - * @param string $informations_reglement Informations concernant le règlement (numéro de chèque, de virement etc.) - * @param int $date_debut Date de début de la - * cotisation - * @param int $date_fin Date de fin de la cotisation - * @param string $commentaires Commentaires concernnant la cotisation - * @param string $referenceClient Reference client à mentionner sur la facture - * @return bool Succès de la modification - */ - public function modifier($id, $type_personne, $id_personne, $montant, $type_reglement, - $informations_reglement, $date_debut, $date_fin, $commentaires, $referenceClient): bool - { - $requete = 'UPDATE'; - $requete .= ' afup_cotisations '; - $requete .= 'SET'; - $requete .= ' type_personne=' . $type_personne . ','; - $requete .= ' id_personne=' . $id_personne . ','; - $requete .= ' montant=' . $montant . ','; - $requete .= ' type_reglement=' . $type_reglement . ','; - $requete .= ' informations_reglement=' . $this->_bdd->echapper($informations_reglement) . ','; - $requete .= ' date_debut=' . $date_debut . ','; - $requete .= ' date_fin=' . $date_fin . ','; - $requete .= ' commentaires=' . $this->_bdd->echapper($commentaires) . ','; - $requete .= ' reference_client=' . $this->_bdd->echapper($referenceClient) . ' '; - $requete .= 'WHERE'; - $requete .= ' id=' . $id; - return $this->_bdd->executer($requete) !== false; - } - - public function estDejaReglee($cmd) - { - $requete = 'SELECT'; - $requete .= ' 1 '; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' informations_reglement=' . $this->_bdd->echapper($cmd); - return $this->_bdd->obtenirUn($requete); - } - - public function notifierReglementEnLigneAuTresorier(string $cmd, float $total, string $autorisation, string $transaction, UserRepository $userRepository): ?bool - { - if (str_starts_with($cmd, 'F')) { - // Facture - $invoiceNumber = substr($cmd, 1); - $cotisation = $this->getByInvoice($invoiceNumber); - $type_personne = $cotisation['type_personne']; - $id_personne = $cotisation['id_personne']; - - } else { - // Cotisation - [$ref, $date, $type_personne, $id_personne, $reste] = explode('-', $cmd, 5); - } - - $infos = [ - 'id' => $id_personne, - 'type' => $type_personne, - 'nom' => 'N.C.', - 'prenom' => 'N.C.', - 'email' => 'N.C.', - ]; - - if ($type_personne == MemberType::MemberCompany->value) { - if ($company = $this->companyMemberRepository?->get($id_personne)) { - $infos['nom'] = $company->getLastName(); - $infos['prenom'] = $company->getFirstName(); - $infos['email'] = $company->getEmail(); - } - } else { - if ($user = $userRepository->get($id_personne)) { - $infos['nom'] = $user->getLastName(); - $infos['prenom'] = $user->getFirstName(); - $infos['email'] = $user->getEmail(); - } - } - - - $sujet = "Paiement cotisation AFUP\n"; - - $corps = "Bonjour, \n\n"; - $corps .= "Une cotisation annuelle AFUP a été réglée.\n\n"; - $corps .= "Personne : " . $infos['nom'] . " " . $infos['prenom'] . " (" . $infos['email'] . ")\n"; - $corps .= "URL : /admin/accounting/membership-fee/list/" . $infos['type'] . "/" . $infos['id'] . "\n"; - $corps .= "Commande : " . $cmd . "\n"; - $corps .= "Total : " . $total . "\n"; - $corps .= "Autorisation : " . $autorisation . "\n"; - $corps .= "Transaction : " . $transaction . "\n\n"; - - $ok = Mailing::envoyerMail(new Message($sujet, new MailUser(MailUser::DEFAULT_SENDER_EMAIL, MailUser::DEFAULT_SENDER_NAME), MailUserFactory::tresorier()), $corps); - - if (false === $ok) { - return false; - } - return null; - } - - public function validerReglementEnLigne($cmd, $total, string $autorisation, string $transaction) - { - $reference = substr((string) $cmd, 0, strlen((string) $cmd) - 4); - $verif = substr((string) $cmd, strlen((string) $cmd) - 3, strlen((string) $cmd)); - $result = false; - - if (str_starts_with((string) $cmd, 'F')) { - // This is an invoice ==> we dont have to create a new cotisation, just update the existing one - $invoiceNumber = substr((string) $cmd, 1); - $cotisation = $this->getByInvoice($invoiceNumber); - - $this - ->updatePayment( - $cotisation['id'], - AFUP_COTISATIONS_REGLEMENT_ENLIGNE, "autorisation : " . $autorisation . " / transaction : " . $transaction, - ); - } elseif (substr(md5($reference), -3) === strtolower($verif) && !$this->estDejaReglee($cmd)) { - [$ref, $date, $type_personne, $id_personne, $reste] = explode('-', (string) $cmd, 5); - $date_debut = mktime(0, 0, 0, (int) substr($date, 2, 2), (int) substr($date, 0, 2), (int) substr($date, 4, 4)); - - $cotisation = $this->obtenirDerniere(MemberType::from((int) $type_personne), $id_personne); - $date_fin_precedente = $cotisation === false ? 0 : $cotisation['date_fin']; - - if ($date_fin_precedente > 0) { - $date_debut = strtotime('+1day', (int) $date_fin_precedente); - } - - $date_fin = $this->finProchaineCotisation($cotisation)->format('U'); - $result = $this->ajouter( - MemberType::from((int) $type_personne), - $id_personne, - $total, - AFUP_COTISATIONS_REGLEMENT_ENLIGNE, - $cmd, - $date_debut, - $date_fin, - "autorisation : " . $autorisation . " / transaction : " . $transaction); - } - - return $result; - } - - /** - * @return array{type: int, id: int} - */ - public function getAccountFromCmd(string $cmd): array - { - $arr = explode('-', $cmd, 5); - // Depuis une facture : $cmd=FCOTIS-2023-202 - if (3 === count($arr)) { - return ['type' => MemberType::MemberCompany->value, 'id' => (int) $arr[2]]; - } - - // Depuis une cotisation : $cmd=C2023-211120232237-0-5-PAUL-431 - [$ref, $date, $memberType, $memberId, $stuff] = $arr; - - return ['type' => (int) $memberType, 'id' => (int) $memberId]; - } - - /** - * Supprime une cotisation - * - * @param int $id Identifiant de la cotisation à supprimer - * @return bool Succès de la suppression - */ - public function supprimer($id) - { - $requete = 'DELETE FROM afup_cotisations WHERE id=' . $id; - return $this->_bdd->executer($requete); - } - - /** - * Génère une facture au format PDF - * - * @param int $id_cotisation Identifiant de la cotisation - * @param string $chemin Chemin du fichier PDF à générer. Si ce chemin est omi, le PDF est renvoyé au navigateur. - * @return int Le numero de la facture - */ - public function genererFacture($id_cotisation, $chemin = null) - { - $requete = 'SELECT * FROM afup_cotisations WHERE id=' . $id_cotisation; - $cotisation = $this->_bdd->obtenirEnregistrement($requete); - - $table = $cotisation['type_personne'] == MemberType::MemberCompany->value ? 'afup_personnes_morales' : 'afup_personnes_physiques'; - $requete = 'SELECT * FROM ' . $table . ' WHERE id=' . $cotisation['id_personne']; - $personne = $this->_bdd->obtenirEnregistrement($requete); - - if ($cotisation['date_facture'] !== null) { - $dateFacture = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $cotisation['date_facture']); - } else { - $dateFacture = \DateTimeImmutable::createFromFormat('U', $cotisation['date_debut']); - } - - $bankAccountFactory = new BankAccountFactory(); - $isSubjectedToVat = Vat::isSubjectedToVat($dateFacture); - // Construction du PDF - $pdf = new PDF_Facture($bankAccountFactory->createApplyableAt($dateFacture), $isSubjectedToVat); - $pdf->AddPage(); - - $pdf->Cell(130, 5); - $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); - - $pdf->Ln(); - $pdf->Ln(); - $pdf->Ln(); - - // A l'attention du client [adresse] - $pdf->SetFont('Arial', 'BU', 10); - $pdf->Cell(130, 5, 'Objet : Facture n°' . $cotisation['numero_facture']); - $pdf->SetFont('Arial', '', 10); - - if ($cotisation['type_personne'] == MemberType::MemberCompany->value) { - $nom = $personne['raison_sociale']; - $patternPrefix = $personne['raison_sociale']; - } else { - $nom = $personne['prenom'] . ' ' . $personne['nom']; - $patternPrefix = $personne['nom']; - } - $pdf->Ln(10); - $pdf->MultiCell(130, 5, $nom . "\n" . $personne['adresse'] . "\n" . $personne['code_postal'] . "\n" . $personne['ville']); - - if (isset($cotisation['reference_client'])) { - $pdf->Ln(10); - $pdf->MultiCell(180, 5, sprintf( - "Référence client : %s", - $cotisation['reference_client'], - )); - } - - $pdf->Ln(15); - - $pdf->MultiCell(180, 5, "Facture concernant votre adhésion à l'Association Française des Utilisateurs de PHP (AFUP)."); - - if (false === $isSubjectedToVat) { - // Cadre - $pdf->Ln(10); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(50, 5, 'Code', 1, 0, 'L', 1); - $pdf->Cell(100, 5, 'Désignation', 1, 0, 'L', 1); - $pdf->Cell(40, 5, 'Prix', 1, 0, 'L', 1); - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(50, 5, 'ADH', 1); - $pdf->Cell(100, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', (int) $cotisation['date_fin']), 1); - $pdf->Cell(40, 5, $cotisation['montant'] . ' €', 1); - - $pdf->Ln(15); - $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); - } else { - // On stocke le montant de la cotisation TTC, pour les personnes physiques c'est le même, par contre pour les personnes morales - // ce n'est pas le même, afin d'éviter d'appliquer deux fois la TVA, on applique ce hotfix - if ($cotisation['type_personne'] == MemberType::MemberCompany->value) { - $cotisation['montant'] = Vat::getRoundedWithoutVatPriceFromPriceWithVat($cotisation['montant'], Utils::MEMBERSHIP_FEE_VAT_RATE); - } - - - // Cadre - $pdf->Ln(10); - $pdf->SetFillColor(200, 200, 200); - $pdf->Cell(20, 5, 'Code', 1, 0, 'L', 1); - $pdf->Cell(95, 5, 'Désignation', 1, 0, 'L', 1); - $pdf->Cell(25, 5, 'Prix HT', 1, 0, 'R', 1); - $pdf->Cell(25, 5, 'Taux TVA', 1, 0, 'R', 1); - $pdf->Cell(25, 5, 'Prix TTC', 1, 0, 'R', 1); - - if ($cotisation['type_personne'] == MemberType::MemberCompany->value) { - [$totalHt, $total] = $this->buildDetailsPersonneMorale($pdf, $cotisation['montant'], $cotisation['date_fin']); - } else { - [$totalHt, $total] = $this->buildDetailsPersonnePhysique($pdf, $cotisation['montant'], $cotisation['date_fin']); - } - - $pdf->Ln(); - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(165, 5, 'Total HT', 1, 0, 'R', 1); - $pdf->Cell(25, 5, $this->formatFactureValue($totalHt) . ' €', 1, 0, 'R', 1); - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(165, 5, 'Total TVA 20%', 1, 0, 'R', 1); - $pdf->Cell(25, 5, $this->formatFactureValue($total - $totalHt) . ' €', 1, 0, 'R', 1); - - $pdf->Ln(); - $pdf->SetFillColor(225, 225, 225); - $pdf->Cell(165, 5, 'Total TTC', 1, 0, 'R', 1); - $pdf->Cell(25, 5, $this->formatFactureValue($total) . ' €', 1, 0, 'R', 1); - } - - $pdf->Ln(15); - $pdf->Cell(10, 5, 'Lors de votre règlement, merci de préciser la mention : "Facture n°' . $cotisation['numero_facture'] . '"'); - - if (is_null($chemin)) { - $pattern = str_replace(' ', '', $patternPrefix) . '_' . $cotisation['numero_facture'] . '_' . date('dmY', (int) $cotisation['date_debut']) . '.pdf'; - - $pdf->Output($pattern, 'D', true); - } else { - $pdf->Output($chemin, 'F', true); - } - - return $cotisation['numero_facture']; - } - - private function buildDetailsPersonneMorale(PDF_Facture $pdf, $montant, $dateFin): array - { - $montantTtc = $montant * (1 + Utils::MEMBERSHIP_FEE_VAT_RATE); - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(20, 5, 'ADH', 1); - $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', (int) $dateFin), 1); - $pdf->Cell(25, 5, $this->formatFactureValue($montant) . ' €', 1, 0, 'R'); - $pdf->Cell(25, 5, (Utils::MEMBERSHIP_FEE_VAT_RATE * 100 . ' %'), 1, 0, 'R'); - $pdf->Cell(25, 5, $this->formatFactureValue($montantTtc) . ' €', 1, 0, 'R'); - $totalHt = $montant; - $total = $montantTtc; - - return [$totalHt, $total]; - } - - private function buildDetailsPersonnePhysique(PDF_Facture $pdf, $montant, $dateFin): array - { - $montantFixeHt = 5 / 100 * $montant; - $montantFixeTTc = $montantFixeHt * (1 + Utils::MEMBERSHIP_FEE_VAT_RATE); - $montantVariable = $montant - $montantFixeTTc; - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(20, 5, 'ADH-var', 1); - $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', (int) $dateFin) . ' - part variable', 1); - $pdf->Cell(25, 5, $this->formatFactureValue($montantFixeHt) . ' €', 1, 0, 'R'); - $pdf->Cell(25, 5, (Utils::MEMBERSHIP_FEE_VAT_RATE * 100 . ' %'), 1, 0, 'R'); - $pdf->Cell(25, 5, $this->formatFactureValue($montantFixeTTc) . ' €', 1, 0, 'R'); - - $pdf->Ln(); - $pdf->SetFillColor(255, 255, 255); - $pdf->Cell(20, 5, 'ADH-fixe', 1); - $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', (int) $dateFin) . ' - part fixe', 1); - $pdf->Cell(25, 5, $this->formatFactureValue($montantVariable) . ' €', 1, 0, 'R'); - $pdf->Cell(25, 5, '0 %', 1, 0, 'R'); - $pdf->Cell(25, 5, $this->formatFactureValue($montantVariable) . ' €', 1, 0, 'R'); - - $totalHt = $montantFixeHt + $montantVariable; - $total = $montantFixeTTc + $montantVariable; - - return [$totalHt, $total]; - } - - private function formatFactureValue($value): string - { - return number_format($value, 2, ',', ' '); - } - - /** - * Envoi par mail d'une facture au format PDF - * - * @param int $id_cotisation Identifiant de la cotisation - * @return bool Succès de l'envoi - */ - public function envoyerFacture($id_cotisation, Mailer $mailer, UserRepository $userRepository) - { - $personne = $this->obtenir($id_cotisation, 'type_personne, id_personne'); - - if ($personne['type_personne'] == MemberType::MemberCompany->value) { - $company = $this->companyMemberRepository ? $this->companyMemberRepository->get($personne['id_personne']) : null; - Assert::notNull($company); - $contactPhysique = [ - 'nom' => $company->getLastName(), - 'prenom' => $company->getFirstName(), - 'email' => $company->getEmail(), - ]; - } else { - $user = $userRepository->get($personne['id_personne']); - Assert::notNull($user); - $contactPhysique = [ - 'nom' => $user->getLastName(), - 'prenom' => $user->getFirstName(), - 'email' => $user->getEmail(), - ]; - } - $patternPrefix = $contactPhysique['nom']; - - $corps = "Bonjour,
"; - $corps .= "

Veuillez trouver ci-joint la facture correspondant à votre adhésion à l'AFUP.

"; - $corps .= "

Nous restons à votre disposition pour toute demande complémentaire.

"; - $corps .= "

Le bureau

"; - $corps .= AFUP_RAISON_SOCIALE . "
"; - $corps .= AFUP_ADRESSE . "
"; - $corps .= AFUP_CODE_POSTAL . " " . AFUP_VILLE . "
"; - - $cheminFacture = AFUP_CHEMIN_RACINE . 'cache/fact' . $id_cotisation . '.pdf'; - $numeroFacture = $this->genererFacture($id_cotisation, $cheminFacture); - $cotisation = $this->obtenirDerniere(MemberType::from((int) $personne['type_personne']), $personne['id_personne']); - $pattern = str_replace(' ', '', $patternPrefix) . '_' . $numeroFacture . '_' . date('dmY', (int) $cotisation['date_debut']) . '.pdf'; - - $message = new Message('Facture AFUP', null, new MailUser( - $contactPhysique['email'], - sprintf('%s %s', $contactPhysique['prenom'], $contactPhysique['nom']), - )); - $message->addAttachment(new Attachment( - $cheminFacture, - $pattern, - 'base64', - 'application/pdf', - )); - $ok = $mailer->sendTransactional($message, $corps); - @unlink($cheminFacture); - - return $ok; - } - - /** - * Retourne la dernière cotisation d'une personne morale - * @param int $id_personne Identifiant de la personne - * @return array|false - */ - public function obtenirDerniere(MemberType $type_personne, $id_personne) - { - $requete = 'SELECT'; - $requete .= ' * '; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' type_personne=' . $type_personne->value . ' '; - $requete .= ' AND id_personne=' . $id_personne . ' '; - $requete .= 'ORDER BY'; - $requete .= ' date_fin DESC '; - $requete .= 'LIMIT 0, 1 '; - return $this->_bdd->obtenirEnregistrement($requete); - } - - /** - * Retourne la date de début d'une cotisation. - * - * Cette date est déterminée par la date de fin de la cotisation précédente - * s'il y en a une ou alors sur la date du jour dans le cas contraire. - * - * @param int $type_personne Identifiant du type de personne - * @param int $id_personne Identifiant de la personne - * @return int Timestamp de la date de la cotisation - */ - public function obtenirDateDebut($type_personne, $id_personne) - { - $requete = 'SELECT'; - $requete .= ' date_fin '; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' type_personne=' . $type_personne . ' '; - $requete .= 'AND'; - $requete .= ' id_personne=' . $id_personne . ' '; - $requete .= 'ORDER BY'; - $requete .= ' date_fin DESC'; - $date_debut = $this->_bdd->obtenirUn($requete); - - if ($date_debut !== false) { - return (int) $date_debut; - } else { - return time(); - } - } - - /** - * @param array|false $cotisation from Afup_Personnes_Physiques::obtenirDerniereCotisation - * @return DateTime Date of end of next subscription - */ - public function finProchaineCotisation($cotisation = false): DateTime - { - $endSubscription = $cotisation === false ? new DateTime() : new \DateTime('@' . $cotisation['date_fin']); - $base = $now = new DateTime(); - - $year = new DateInterval('P1Y'); - - if ($endSubscription > $now) { - $base = $endSubscription; - } - - $base->add($year); - return $base; - } - - /** - * Renvoit la cotisation demandée - * - * @param string $invoiceId Identifiant de la facture - * @param string|null $token Token de la facture. Si null, pas de vérification - * @return array - */ - public function getByInvoice(string $invoiceId, string $token = null) - { - $requete = 'SELECT'; - $requete .= ' * '; - $requete .= 'FROM'; - $requete .= ' afup_cotisations '; - $requete .= 'WHERE'; - $requete .= ' numero_facture = ' . $this->_bdd->echapper($invoiceId); - if ($token !== null) { - $requete .= ' AND token = ' . $this->_bdd->echapper($token); - } - - return $this->_bdd->obtenirEnregistrement($requete); - } - - /** - * Modifie une cotisation - * - * @param int $id Identifiant de la cotisation à modifier - * @param int $type_reglement Type de règlement (espèces, chèque, virement) - * @param string $informations_reglement Informations concernant le règlement (numéro de chèque, de virement etc.) - * @return bool Succès de la modification - */ - public function updatePayment($id, $type_reglement, string $informations_reglement): bool - { - $requete = 'UPDATE'; - $requete .= ' afup_cotisations '; - $requete .= 'SET'; - $requete .= ' type_reglement=' . $type_reglement . ','; - $requete .= ' informations_reglement=' . $this->_bdd->echapper($informations_reglement); - $requete .= ' WHERE'; - $requete .= ' id=' . $id; - return $this->_bdd->executer($requete) !== false; - } - - public function isCurrentUserAllowedToReadInvoice(string $invoiceId) - { - if (!$this->_droits) { - throw new \RuntimeException('La variable $_droits ne doit pas être null.'); - } - - $sql = 'SELECT type_personne, id_personne FROM afup_cotisations WHERE id = ' . $this->_bdd->echapper($invoiceId); - $result = $this->_bdd->obtenirEnregistrement($sql); - - if (!$result) { - return false; - } - - /** - * si type_personne = 0, alors personne physique: id_personne doit être identique l'id de l'utilisateur connecté - */ - if ($result['type_personne'] === "0") { - return $result['id_personne'] == $this->_droits->obtenirIdentifiant(); - } - - /** - * si type_personne = 1, alors personne morale: id_personne doit être égale à compagnyId de l'utilisateur connecté - * qui doit aussi avoir le droit "ROLE_COMPAGNY_MANAGER" - */ - if ($result['type_personne'] == MemberType::MemberCompany->value) { - return $this->_droits->verifierDroitManagerPersonneMorale($result['id_personne']); - } - - return false; - } - - public function setCompanyMemberRepository(CompanyMemberRepository $companyMemberRepository): void - { - $this->companyMemberRepository = $companyMemberRepository; - } -} diff --git a/sources/Afup/Association/CotisationsFactory.php b/sources/Afup/Association/CotisationsFactory.php deleted file mode 100644 index 804b0cb18..000000000 --- a/sources/Afup/Association/CotisationsFactory.php +++ /dev/null @@ -1,31 +0,0 @@ -tokenStorage, $this->authorizationChecker), - ); - - $cotisations->setCompanyMemberRepository($this->companyMemberRepository); - - return $cotisations; - } -} diff --git a/sources/AppBundle/Accounting/Invoices/Dto/InvoiceData.php b/sources/AppBundle/Accounting/Invoices/Dto/InvoiceData.php new file mode 100644 index 000000000..ed3517643 --- /dev/null +++ b/sources/AppBundle/Accounting/Invoices/Dto/InvoiceData.php @@ -0,0 +1,16 @@ +getCompanyName(), + $member->getAddress(), + $member->getZipcode(), + $member->getCity(), + $member->getCompanyName(), + ); + } + + public function support(User|CompanyMember $user): bool + { + return $user instanceof CompanyMember; + } +} diff --git a/sources/AppBundle/Accounting/Invoices/Generator/InvoiceGeneration.php b/sources/AppBundle/Accounting/Invoices/Generator/InvoiceGeneration.php new file mode 100644 index 000000000..e9a0f0ca5 --- /dev/null +++ b/sources/AppBundle/Accounting/Invoices/Generator/InvoiceGeneration.php @@ -0,0 +1,16 @@ +getFirstName() . ' ' . $member->getLastName(), + $member->getAddress(), + $member->getZipcode(), + $member->getCity(), + $member->getLastName(), + ); + } + + public function support(User|CompanyMember $user): bool + { + return $user instanceof User; + } +} diff --git a/sources/AppBundle/Accounting/Invoices/InvoiceGenerator.php b/sources/AppBundle/Accounting/Invoices/InvoiceGenerator.php new file mode 100644 index 000000000..d13d024de --- /dev/null +++ b/sources/AppBundle/Accounting/Invoices/InvoiceGenerator.php @@ -0,0 +1,32 @@ + $handlers + */ + public function __construct(private readonly iterable $handlers) {} + + public function getInvoiceData(User|CompanyMember $user): InvoiceData + { + foreach ($this->handlers as $handler) { + if ($handler->support($user)) { + return $handler->generate($user); + } + } + throw new \RuntimeException(sprintf( + 'No invoice generator supports member of type %s (id: %d).', + $user::class, + $user->getId(), + )); + } +} diff --git a/sources/AppBundle/Association/CompanyMembership/SubscriptionManagement.php b/sources/AppBundle/Association/CompanyMembership/SubscriptionManagement.php index 1c27c35c4..576afb378 100644 --- a/sources/AppBundle/Association/CompanyMembership/SubscriptionManagement.php +++ b/sources/AppBundle/Association/CompanyMembership/SubscriptionManagement.php @@ -4,36 +4,37 @@ namespace AppBundle\Association\CompanyMembership; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; use Afup\Site\Utils\Utils; use AppBundle\Association\MemberType; use AppBundle\Association\Model\CompanyMember; +use AppBundle\MembershipFee\Model\MembershipFee; final readonly class SubscriptionManagement { - public function __construct(private Cotisations $cotisations) {} + public function __construct(private MembershipFeeService $membershipFeeService) {} - public function createInvoiceForInscription(CompanyMember $company, $numberOfMembers): array + public function createInvoiceForInscription(CompanyMember $company, int $numberOfMembers): array { - $endSubscription = $this->cotisations->finProchaineCotisation(false); + $endSubscription = $this->membershipFeeService->getNextSubscriptionExpiration(null); // Create the invoice - $this->cotisations->ajouter( + $this->membershipFeeService->ajouter( MemberType::MemberCompany, $company->getId(), ceil($numberOfMembers / AFUP_PERSONNE_MORALE_SEUIL) * AFUP_COTISATION_PERSONNE_MORALE * (1 + Utils::MEMBERSHIP_FEE_VAT_RATE), null, null, - (new \DateTime())->format('U'), - $endSubscription->format('U'), + (new \DateTime())->getTimestamp(), + $endSubscription->getTimestamp(), '', ); - $subscriptionArray = $this->cotisations->obtenirDerniere(MemberType::MemberCompany, $company->getId()); + $subscription = $this->membershipFeeService->getLatestByUserTypeAndId(MemberType::MemberCompany, $company->getId()); - if ($subscriptionArray === false) { + if (!$subscription instanceof MembershipFee) { throw new \RuntimeException('An error occured'); } - return ['invoice' => $subscriptionArray['numero_facture'], 'token' => $subscriptionArray['token']]; + return ['invoice' => $subscription->getInvoiceNumber(), 'token' => $subscription->getToken()]; } } diff --git a/sources/AppBundle/Association/UserMembership/SeniorityComputer.php b/sources/AppBundle/Association/UserMembership/SeniorityComputer.php index 371211a5d..39860457b 100644 --- a/sources/AppBundle/Association/UserMembership/SeniorityComputer.php +++ b/sources/AppBundle/Association/UserMembership/SeniorityComputer.php @@ -4,18 +4,20 @@ namespace AppBundle\Association\UserMembership; -use Afup\Site\Association\Cotisations; use AppBundle\Association\MemberType; use AppBundle\Association\Model\CompanyMember; use AppBundle\Association\Model\User; +use AppBundle\MembershipFee\Model\MembershipFee; +use AppBundle\MembershipFee\Model\Repository\MembershipFeeRepository; +use CCMBenchmark\Ting\Repository\Collection; class SeniorityComputer { - public function __construct(private readonly Cotisations $cotisations) {} + public function __construct(private readonly MembershipFeeRepository $membershipFeeRepository) {} public function computeCompany(CompanyMember $companyMember) { - $cotis = $this->cotisations->obtenirListe(MemberType::MemberCompany, $companyMember->getId()); + $cotis = $this->membershipFeeRepository->getListByUserTypeAndId(MemberType::MemberCompany, $companyMember->getId()); $infos = $this->computeFromCotisationsAndReturnInfos($cotis); @@ -24,7 +26,7 @@ public function computeCompany(CompanyMember $companyMember) public function computeCompanyAndReturnInfos(CompanyMember $companyMember): array { - $cotis = $this->cotisations->obtenirListe(MemberType::MemberCompany, $companyMember->getId()); + $cotis = $this->membershipFeeRepository->getListByUserTypeAndId(MemberType::MemberCompany, $companyMember->getId()); return $this->computeFromCotisationsAndReturnInfos($cotis); } @@ -38,20 +40,23 @@ public function compute(User $user) public function computeAndReturnInfos(User $user): array { - $cotis = $this->cotisations->obtenirListe(MemberType::MemberPhysical, $user->getId()); + $cotis = $this->membershipFeeRepository->getListByUserTypeAndId(MemberType::MemberPhysical, $user->getId()); return $this->computeFromCotisationsAndReturnInfos($cotis); } - private function computeFromCotisationsAndReturnInfos(array $cotisations): array + /** + * @param Collection $cotisations + */ + private function computeFromCotisationsAndReturnInfos(Collection $cotisations): array { $now = new \DateTime(); $diffs = []; $years = []; foreach ($cotisations as $coti) { - $from = new \DateTimeImmutable('@' . $coti['date_debut']); - $to = new \DateTimeImmutable('@' . $coti['date_fin']); + $from = $coti->getStartDate(); + $to = $coti->getEndDate(); $to = min($now, $to); $diffs[] = $from->diff($to); $years[] = $from->format('Y'); diff --git a/sources/AppBundle/Association/UserMembership/UserService.php b/sources/AppBundle/Association/UserMembership/UserService.php index caa9375d3..149b2a022 100644 --- a/sources/AppBundle/Association/UserMembership/UserService.php +++ b/sources/AppBundle/Association/UserMembership/UserService.php @@ -4,7 +4,7 @@ namespace AppBundle\Association\UserMembership; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; use AppBundle\Association\MemberType; use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\Association\Model\User; @@ -12,6 +12,7 @@ use AppBundle\Email\Mailer\MailUser; use AppBundle\Email\Mailer\MailUserFactory; use AppBundle\Email\Mailer\Message; +use AppBundle\MembershipFee\Model\MembershipFee; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -23,7 +24,7 @@ public function __construct( private readonly UserRepository $userRepository, private readonly Mailer $mailer, private readonly UrlGeneratorInterface $urlGenerator, - private readonly Cotisations $cotisations, + private readonly MembershipFeeService $membershipFeeService, private readonly UserPasswordHasherInterface $passwordHasher, ) {} @@ -85,10 +86,7 @@ public function sendWelcomeEmail(User $user): bool return $this->mailer->send($message); } - /** - * @return array - */ - public function getLastSubscription(User $user) + public function getLastSubscription(User $user): ?MembershipFee { if ($user->getCompanyId()) { $id = $user->getCompanyId(); @@ -98,6 +96,6 @@ public function getLastSubscription(User $user) $personType = MemberType::MemberPhysical; } - return $this->cotisations->obtenirDerniere($personType, $id); + return $this->membershipFeeService->getLatestByUserTypeAndId($personType, $id); } } diff --git a/sources/AppBundle/Command/PayboxCallbackSimulatorCommand.php b/sources/AppBundle/Command/PayboxCallbackSimulatorCommand.php index a541402f5..f6fe35e49 100644 --- a/sources/AppBundle/Command/PayboxCallbackSimulatorCommand.php +++ b/sources/AppBundle/Command/PayboxCallbackSimulatorCommand.php @@ -4,7 +4,8 @@ namespace AppBundle\Command; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; +use AppBundle\MembershipFee\OnlinePaymentHandler; use AppBundle\Association\MemberType; use AppBundle\Event\Model\Event; use AppBundle\Event\Model\Invoice; @@ -30,7 +31,8 @@ class PayboxCallbackSimulatorCommand extends Command public function __construct( private readonly InvoiceRepository $invoiceRepository, private readonly EventRepository $eventRepository, - private readonly Cotisations $cotisations, + private readonly MembershipFeeService $membershipFeeService, + private readonly OnlinePaymentHandler $onlinePaymentHandler, private readonly UrlGeneratorInterface $urlGenerator, ) { parent::__construct(); @@ -104,8 +106,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function callCotisation(string $cmd, string $status): string { - $account = $this->cotisations->getAccountFromCmd($cmd); - $cotisation = $this->cotisations->obtenirDerniere(MemberType::from($account['type']), $account['id']); + $account = $this->onlinePaymentHandler->getAccountFromCmd($cmd); + $cotisation = $this->membershipFeeService->getLatestByUserTypeAndId(MemberType::from($account['type']), $account['id']); if (!$cotisation) { throw new \RuntimeException( sprintf('Cotisation non trouvée avec ce CMD: %s', $cmd), @@ -113,7 +115,7 @@ private function callCotisation(string $cmd, string $status): string } $url = $this->urlGenerator->generate('membership_payment'); - return $this->buildUrl($url, (float) $cotisation['montant'], $cmd, $status); + return $this->buildUrl($url, $cotisation->getAmount(), $cmd, $status); } private function callInvoice(string $cmd, string $status): string diff --git a/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/DownloadMembershipFeeInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/DownloadMembershipFeeInvoiceAction.php index c36d21d3f..60f9206ff 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/DownloadMembershipFeeInvoiceAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/DownloadMembershipFeeInvoiceAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Admin\Accounting\MembershipFee; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeInvoicePdfGenerator; use AppBundle\Association\MemberType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -12,13 +12,13 @@ class DownloadMembershipFeeInvoiceAction extends AbstractController { public function __construct( - private readonly Cotisations $cotisations, + private readonly MembershipFeeInvoicePdfGenerator $pdfGenerator, ) {} public function __invoke(MemberType $memberType, int $memberId, int $membershipFeeId): Response { ob_start(); - $this->cotisations->genererFacture($membershipFeeId); + $this->pdfGenerator->genererFacture($membershipFeeId); $pdf = ob_get_clean(); $response = new Response($pdf); diff --git a/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/SendMembershipFeeInvoiceAction.php b/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/SendMembershipFeeInvoiceAction.php index aad3df00d..cab1504f0 100644 --- a/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/SendMembershipFeeInvoiceAction.php +++ b/sources/AppBundle/Controller/Admin/Accounting/MembershipFee/SendMembershipFeeInvoiceAction.php @@ -4,26 +4,22 @@ namespace AppBundle\Controller\Admin\Accounting\MembershipFee; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeMailer; use AppBundle\Association\MemberType; -use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\AuditLog\Audit; -use AppBundle\Email\Mailer\Mailer; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; class SendMembershipFeeInvoiceAction extends AbstractController { public function __construct( - private readonly Mailer $mailer, - private readonly UserRepository $userRepository, - private readonly Cotisations $cotisations, + private readonly MembershipFeeMailer $membershipFeeMailer, private readonly Audit $audit, ) {} public function __invoke(MemberType $memberType, int $memberId, int $membershipFeeId): Response { - if ($this->cotisations->envoyerFacture($membershipFeeId, $this->mailer, $this->userRepository)) { + if ($this->membershipFeeMailer->envoyerFacture($membershipFeeId)) { $this->audit->log('Envoi par email de la facture pour la cotisation n°' . $membershipFeeId); $this->addFlash('notice', 'La facture a été envoyée'); } else { diff --git a/sources/AppBundle/Controller/Website/Member/IndexAction.php b/sources/AppBundle/Controller/Website/Member/IndexAction.php index d5ac0ed0f..e9a7087ca 100644 --- a/sources/AppBundle/Controller/Website/Member/IndexAction.php +++ b/sources/AppBundle/Controller/Website/Member/IndexAction.php @@ -10,6 +10,7 @@ use AppBundle\Association\UserMembership\BadgesComputer; use AppBundle\Association\UserMembership\UserService; use AppBundle\GeneralMeeting\GeneralMeetingRepository; +use AppBundle\MembershipFee\Model\MembershipFee; use AppBundle\Security\Authentication; use AppBundle\Twig\ViewRenderer; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -38,8 +39,8 @@ public function __invoke(): Response $cotisation = $userService->getLastSubscription($user); $dateFinCotisation = null; - if ($cotisation) { - $dateFinCotisation = new \DateTimeImmutable('@' . $cotisation['date_fin']); + if ($cotisation instanceof MembershipFee) { + $dateFinCotisation = $cotisation->getEndDate(); } $daysBeforeMembershipExpiration = $user->getDaysBeforeMembershipExpiration(); diff --git a/sources/AppBundle/Controller/Website/Membership/Fee/DownloadAction.php b/sources/AppBundle/Controller/Website/Membership/Fee/DownloadAction.php index 61090b571..768a8a481 100644 --- a/sources/AppBundle/Controller/Website/Membership/Fee/DownloadAction.php +++ b/sources/AppBundle/Controller/Website/Membership/Fee/DownloadAction.php @@ -4,7 +4,7 @@ namespace AppBundle\Controller\Website\Membership\Fee; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeInvoicePdfGenerator; use Afup\Site\Droits; use AppBundle\Association\MemberType; use AppBundle\Association\Model\CompanyMember; @@ -12,6 +12,8 @@ use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\Association\Model\User; use AppBundle\AuditLog\Audit; +use AppBundle\MembershipFee\Model\Repository\MembershipFeeRepository; +use AppBundle\Security\MembershipFeeVoter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; @@ -23,7 +25,8 @@ final class DownloadAction extends AbstractController public function __construct( private readonly UserRepository $userRepository, private readonly CompanyMemberRepository $companyMemberRepository, - private readonly Cotisations $cotisations, + private readonly MembershipFeeRepository $membershipFeeRepository, + private readonly MembershipFeeInvoicePdfGenerator $pdfGenerator, private readonly Droits $droits, private readonly Audit $audit, ) {} @@ -31,28 +34,28 @@ public function __construct( public function __invoke(Request $request): BinaryFileResponse { $identifiant = $this->droits->obtenirIdentifiant(); - $id = $request->get('id'); + $id = $request->query->getInt('id'); - if (false === $this->cotisations->isCurrentUserAllowedToReadInvoice($id)) { + if (false === $this->isGranted(MembershipFeeVoter::READ_INVOICE, $id)) { $this->audit->log("L'utilisateur id: " . $identifiant . ' a tenté de voir la facture id:' . $id); throw $this->createAccessDeniedException('Cette facture ne vous appartient pas, vous ne pouvez la visualiser.'); } $tempfile = tempnam(sys_get_temp_dir(), 'membership_fee_download'); - $numeroFacture = $this->cotisations->genererFacture($id, $tempfile); - $cotisation = $this->cotisations->obtenir($id); + $numeroFacture = $this->pdfGenerator->genererFacture($id, $tempfile); + $membershipFee = $this->membershipFeeRepository->get($id); - if ($cotisation['type_personne'] == MemberType::MemberCompany->value) { - $company = $this->companyMemberRepository->get($cotisation['id_personne']); + if ($membershipFee->getUserType() === MemberType::MemberCompany) { + $company = $this->companyMemberRepository->get($membershipFee->getUserId()); Assert::isInstanceOf($company, CompanyMember::class); $patternPrefix = $company->getCompanyName(); } else { - $user = $this->userRepository->get($cotisation['id_personne']); + $user = $this->userRepository->get($membershipFee->getUserId()); Assert::isInstanceOf($user, User::class); $patternPrefix = $user->getLastName(); } - $pattern = str_replace(' ', '', $patternPrefix) . '_' . $numeroFacture . '_' . date('dmY', (int) $cotisation['date_debut']) . '.pdf'; + $pattern = str_replace(' ', '', $patternPrefix) . '_' . $numeroFacture . '_' . $membershipFee->getStartDate()->format('dmY') . '.pdf'; $response = new BinaryFileResponse($tempfile, Response::HTTP_OK, [], false); $response->deleteFileAfterSend(true); diff --git a/sources/AppBundle/Controller/Website/Membership/Fee/IndexAction.php b/sources/AppBundle/Controller/Website/Membership/Fee/IndexAction.php index 919b5e514..dcf795717 100644 --- a/sources/AppBundle/Controller/Website/Membership/Fee/IndexAction.php +++ b/sources/AppBundle/Controller/Website/Membership/Fee/IndexAction.php @@ -4,15 +4,18 @@ namespace AppBundle\Controller\Website\Membership\Fee; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; use Afup\Site\Droits; use Afup\Site\Utils\Utils; use Afup\Site\Utils\Vat; +use AppBundle\Security\MembershipFeeVoter; use AppBundle\Association\MembershipFeeReferenceGenerator; use AppBundle\Association\MemberType; use AppBundle\Association\Model\Repository\CompanyMemberRepository; use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\Association\UserMembership\UserService; +use AppBundle\MembershipFee\Model\MembershipFee; +use AppBundle\MembershipFee\Model\Repository\MembershipFeeRepository; use AppBundle\Payment\PayboxBilling; use AppBundle\Payment\PayboxFactory; use AppBundle\Twig\ViewRenderer; @@ -28,7 +31,8 @@ public function __construct( private readonly CompanyMemberRepository $companyMemberRepository, private readonly UserService $userService, private readonly PayboxFactory $payboxFactory, - private readonly Cotisations $cotisations, + private readonly MembershipFeeService $membershipFeeService, + private readonly MembershipFeeRepository $membershipFeeRepository, private readonly Droits $droits, ) {} @@ -43,34 +47,27 @@ public function __invoke(): Response $now = new \DateTime('now'); $isSubjectedToVat = Vat::isSubjectedToVat($now); - if (!$cotisation) { + if (!$cotisation instanceof MembershipFee) { $message = ''; } else { - $endSubscription = $this->cotisations->finProchaineCotisation($cotisation); + $endSubscription = $this->membershipFeeService->getNextSubscriptionExpiration($cotisation); $message = sprintf( 'Votre dernière cotisation -- %s € -- est valable jusqu\'au %s.
Si vous renouvelez votre cotisation maintenant, celle-ci sera valable jusqu\'au %s.', - number_format((float) $cotisation['montant'], 2, ',', ' '), - date("d/m/Y", (int) $cotisation['date_fin']), + number_format((float) $cotisation->getAmount(), 2, ',', ' '), + $cotisation->getEndDate()->format('d/m/Y'), $endSubscription->format('d/m/Y'), ); } - $cotisations_physique = $this->cotisations->obtenirListe(MemberType::MemberPhysical, $user->getId()); - $cotisations_morale = $this->cotisations->obtenirListe(MemberType::MemberCompany, $user->getCompanyId()); + $cotisations_physique = $this->membershipFeeRepository->getListByUserTypeAndId(MemberType::MemberPhysical, $user->getId()); + $cotisations_morale = $this->membershipFeeRepository->getListByUserTypeAndId(MemberType::MemberCompany, $user->getCompanyId()); - if (is_array($cotisations_morale) && is_array($cotisations_physique)) { - $liste_cotisations = array_merge($cotisations_physique, $cotisations_morale); - } elseif (is_array($cotisations_morale)) { - $liste_cotisations = $cotisations_morale; - } elseif (is_array($cotisations_physique)) { - $liste_cotisations = $cotisations_physique; - } else { - $liste_cotisations = []; - } + /** @var array $liste_cotisations */ + $liste_cotisations = array_merge(iterator_to_array($cotisations_physique), iterator_to_array($cotisations_morale)); foreach ($liste_cotisations as $k => $cotisation) { - $liste_cotisations[$k]['telecharger_facture'] = $this->cotisations->isCurrentUserAllowedToReadInvoice($cotisation['id']); + $cotisation->setDownloadInvoice($this->isGranted(MembershipFeeVoter::READ_INVOICE, (string) $cotisation->getId())); } if ($user->getCompanyId() > 0) { @@ -112,7 +109,7 @@ public function __invoke(): Response 'isSubjectedToVat' => $isSubjectedToVat, 'title' => 'Ma cotisation', 'cotisations' => $liste_cotisations, - 'time' => time(), + 'time' => new \DateTime(), 'montant' => $montant, 'libelle' => $libelle, 'paybox' => $paybox, diff --git a/sources/AppBundle/Controller/Website/Membership/Fee/SendMailAction.php b/sources/AppBundle/Controller/Website/Membership/Fee/SendMailAction.php index 78484f1bd..f848be546 100644 --- a/sources/AppBundle/Controller/Website/Membership/Fee/SendMailAction.php +++ b/sources/AppBundle/Controller/Website/Membership/Fee/SendMailAction.php @@ -4,11 +4,10 @@ namespace AppBundle\Controller\Website\Membership\Fee; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeMailer; use Afup\Site\Droits; -use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\AuditLog\Audit; -use AppBundle\Email\Mailer\Mailer; +use AppBundle\Security\MembershipFeeVoter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -16,9 +15,7 @@ final class SendMailAction extends AbstractController { public function __construct( - private readonly UserRepository $userRepository, - private readonly Mailer $mailer, - private readonly Cotisations $cotisations, + private readonly MembershipFeeMailer $membershipFeeMailer, private readonly Droits $droits, private readonly Audit $audit, ) {} @@ -26,14 +23,14 @@ public function __construct( public function __invoke(Request $request): RedirectResponse { $identifiant = $this->droits->obtenirIdentifiant(); - $id = $request->get('id'); + $id = $request->query->getInt('id'); - if (false === $this->cotisations->isCurrentUserAllowedToReadInvoice($id)) { + if (false === $this->isGranted(MembershipFeeVoter::READ_INVOICE, $id)) { $this->audit->log("L'utilisateur id: " . $identifiant . ' a tenté de voir la facture id:' . $id); throw $this->createAccessDeniedException('Cette facture ne vous appartient pas, vous ne pouvez la visualiser.'); } - if ($this->cotisations->envoyerFacture($id, $this->mailer, $this->userRepository)) { + if ($this->membershipFeeMailer->envoyerFacture($id)) { $this->audit->log('Envoi par email de la facture pour la cotisation n°' . $id); $this->addFlash('success', 'La facture a été envoyée par mail'); } else { diff --git a/sources/AppBundle/Controller/Website/Membership/GeneralMeeting/IndexAction.php b/sources/AppBundle/Controller/Website/Membership/GeneralMeeting/IndexAction.php index 6e442eca4..a6c7c0b60 100644 --- a/sources/AppBundle/Controller/Website/Membership/GeneralMeeting/IndexAction.php +++ b/sources/AppBundle/Controller/Website/Membership/GeneralMeeting/IndexAction.php @@ -49,7 +49,7 @@ public function __invoke(Request $request): Response $generalMeetingPlanned = $generalMeetingRepository->hasGeneralMeetingPlanned(); $cotisation = $userService->getLastSubscription($user); - $needsMembersheepFeePayment = $latestDate->getTimestamp() > strtotime("+14 day", (int) $cotisation['date_fin']); + $needsMembersheepFeePayment = $latestDate->getTimestamp() > strtotime("+14 day", $cotisation->getEndDate()->getTimestamp()); if ($needsMembersheepFeePayment) { return $this->view->render('admin/association/membership/generalmeeting_membersheepfee.html.twig', [ diff --git a/sources/AppBundle/Controller/Website/Membership/InvoiceAction.php b/sources/AppBundle/Controller/Website/Membership/InvoiceAction.php index 718290a78..d65d1fe1a 100644 --- a/sources/AppBundle/Controller/Website/Membership/InvoiceAction.php +++ b/sources/AppBundle/Controller/Website/Membership/InvoiceAction.php @@ -4,26 +4,29 @@ namespace AppBundle\Controller\Website\Membership; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; +use AppBundle\MembershipFee\MembershipFeeInvoicePdfGenerator; +use AppBundle\MembershipFee\Model\MembershipFee; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; final class InvoiceAction extends AbstractController { public function __construct( - private readonly Cotisations $cotisations, + private readonly MembershipFeeService $membershipFeeService, + private readonly MembershipFeeInvoicePdfGenerator $pdfGenerator, ) {} public function __invoke(string $invoiceNumber, ?string $token): Response { - $invoice = $this->cotisations->getByInvoice($invoiceNumber, $token); + $invoice = $this->membershipFeeService->getByInvoice($invoiceNumber, $token); - if (!$invoice) { + if (!$invoice instanceof MembershipFee) { throw $this->createNotFoundException(sprintf('Could not find the invoice "%s" with token "%s"', $invoiceNumber, $token)); } ob_start(); - $this->cotisations->genererFacture($invoice['id']); + $this->pdfGenerator->genererFacture($invoice->getId()); $pdf = ob_get_clean(); $response = new Response($pdf); diff --git a/sources/AppBundle/Controller/Website/Membership/PayboxCallbackAction.php b/sources/AppBundle/Controller/Website/Membership/PayboxCallbackAction.php index aef8d5b27..454f4d2bb 100644 --- a/sources/AppBundle/Controller/Website/Membership/PayboxCallbackAction.php +++ b/sources/AppBundle/Controller/Website/Membership/PayboxCallbackAction.php @@ -4,12 +4,14 @@ namespace AppBundle\Controller\Website\Membership; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; +use AppBundle\MembershipFee\MembershipFeeMailer; +use AppBundle\MembershipFee\OnlinePaymentHandler; use AppBundle\Association\Event\NewMemberEvent; use AppBundle\Association\MemberType; -use AppBundle\Association\Model\Repository\CompanyMemberRepository; use AppBundle\Association\Model\Repository\UserRepository; use AppBundle\AuditLog\Audit; +use AppBundle\MembershipFee\Model\MembershipFee; use AppBundle\Payment\PayboxResponseFactory; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; @@ -20,15 +22,15 @@ public function __construct( private EventDispatcherInterface $eventDispatcher, private UserRepository $userRepository, - private CompanyMemberRepository $companyMemberRepository, - private Cotisations $cotisations, + private MembershipFeeService $membershipFeeService, + private OnlinePaymentHandler $onlinePaymentHandler, + private MembershipFeeMailer $membershipFeeMailer, private Audit $audit, ) {} public function __invoke(Request $request): Response { $payboxResponse = PayboxResponseFactory::createFromRequest($request); - $this->cotisations->setCompanyMemberRepository($this->companyMemberRepository); $status = $payboxResponse->getStatus(); $etat = AFUP_COTISATIONS_PAIEMENT_ERREUR; @@ -45,16 +47,16 @@ public function __invoke(Request $request): Response } if ($etat == AFUP_COTISATIONS_PAIEMENT_REGLE) { - $account = $this->cotisations->getAccountFromCmd($payboxResponse->getCmd()); - $lastCotisation = $this->cotisations->obtenirDerniere(MemberType::from($account['type']), $account['id']); + $account = $this->onlinePaymentHandler->getAccountFromCmd($payboxResponse->getCmd()); + $lastCotisation = $this->membershipFeeService->getLatestByUserTypeAndId(MemberType::from($account['type']), $account['id']); - if ($lastCotisation === false && $account['type'] == MemberType::MemberPhysical->value) { + if (!$lastCotisation instanceof MembershipFee && $account['type'] == MemberType::MemberPhysical->value) { $user = $this->userRepository->get($account['id']); $this->eventDispatcher->dispatch(new NewMemberEvent($user)); } - $this->cotisations->validerReglementEnLigne($payboxResponse->getCmd(), round($payboxResponse->getTotal() / 100, 2), $payboxResponse->getAuthorizationId(), $payboxResponse->getTransactionId()); - $this->cotisations->notifierReglementEnLigneAuTresorier($payboxResponse->getCmd(), round($payboxResponse->getTotal() / 100, 2), $payboxResponse->getAuthorizationId(), $payboxResponse->getTransactionId(), $this->userRepository); + $this->onlinePaymentHandler->validerReglementEnLigne($payboxResponse->getCmd(), round($payboxResponse->getTotal() / 100, 2), $payboxResponse->getAuthorizationId(), $payboxResponse->getTransactionId()); + $this->membershipFeeMailer->notifierReglementEnLigneAuTresorier($payboxResponse->getCmd(), round($payboxResponse->getTotal() / 100, 2), $payboxResponse->getAuthorizationId(), $payboxResponse->getTransactionId()); $this->audit->log("Ajout de la cotisation " . $payboxResponse->getCmd() . " via Paybox."); } return new Response(); diff --git a/sources/AppBundle/Controller/Website/Membership/PaymentAction.php b/sources/AppBundle/Controller/Website/Membership/PaymentAction.php index 50b09708c..73754a102 100644 --- a/sources/AppBundle/Controller/Website/Membership/PaymentAction.php +++ b/sources/AppBundle/Controller/Website/Membership/PaymentAction.php @@ -4,9 +4,10 @@ namespace AppBundle\Controller\Website\Membership; -use Afup\Site\Association\Cotisations; +use AppBundle\MembershipFee\MembershipFeeService; use AppBundle\Association\Model\Repository\CompanyMemberRepository; use AppBundle\Compta\BankAccount\BankAccountFactory; +use AppBundle\MembershipFee\Model\MembershipFee; use AppBundle\Payment\PayboxBilling; use AppBundle\Payment\PayboxFactory; use AppBundle\Twig\ViewRenderer; @@ -19,13 +20,16 @@ public function __construct( private readonly ViewRenderer $view, private readonly CompanyMemberRepository $companyMemberRepository, private readonly PayboxFactory $payboxFactory, - private readonly Cotisations $cotisations, + private readonly MembershipFeeService $membershipFeeService, ) {} public function __invoke(string $invoiceNumber, ?string $token): Response { - $invoice = $this->cotisations->getByInvoice($invoiceNumber, $token); - $company = $this->companyMemberRepository->get($invoice['id_personne']); + $invoice = $this->membershipFeeService->getByInvoice($invoiceNumber, $token); + $company = null; + if ($invoice instanceof MembershipFee) { + $company = $this->companyMemberRepository->get($invoice->getUserId()); + } if (!$invoice || $company === null) { throw $this->createNotFoundException(sprintf('Could not find the invoice "%s" with token "%s"', $invoiceNumber, $token)); @@ -35,7 +39,7 @@ public function __invoke(string $invoiceNumber, ?string $token): Response $paybox = $this->payboxFactory->createPayboxForSubscription( 'F' . $invoiceNumber, - (float) $invoice['montant'], + (float) $invoice->getAmount(), $company->getEmail(), $payboxBilling, ); @@ -45,7 +49,7 @@ public function __invoke(string $invoiceNumber, ?string $token): Response return $this->view->render('site/company_membership/payment.html.twig', [ 'paybox' => $paybox, 'invoice' => $invoice, - 'bankAccount' => $bankAccountFactory->createApplyableAt(new \DateTimeImmutable('@' . $invoice['date_debut'])), + 'bankAccount' => $bankAccountFactory->createApplyableAt(new \DateTimeImmutable('@' . $invoice->getStartDate()->getTimestamp())), 'afup' => [ 'raison_sociale' => AFUP_RAISON_SOCIALE, 'adresse' => AFUP_ADRESSE, diff --git a/sources/AppBundle/MembershipFee/MembershipFeeInvoicePdfGenerator.php b/sources/AppBundle/MembershipFee/MembershipFeeInvoicePdfGenerator.php new file mode 100644 index 000000000..18907757a --- /dev/null +++ b/sources/AppBundle/MembershipFee/MembershipFeeInvoicePdfGenerator.php @@ -0,0 +1,191 @@ +membershipFeeRepository->get($idCotisation); + + $userRepository = match ($cotisation->getUserType()) { + MemberType::MemberCompany => $this->companyMemberRepository, + default => $this->userRepository, + }; + + /** @var User|CompanyMember $user */ + $user = $userRepository->get($cotisation->getUserId()); + $invoiceData = $this->invoiceGenerator->getInvoiceData($user); + + $dateFacture = $cotisation->getInvoiceDate() + ?? \DateTimeImmutable::createFromFormat('U', (string) $cotisation->getStartDate()->getTimestamp()); + + $isSubjectedToVat = Vat::isSubjectedToVat($dateFacture); + $pdf = new PDF_Facture($this->bankAccountFactory->createApplyableAt($dateFacture), $isSubjectedToVat); + $pdf->AddPage(); + + $pdf->Cell(130, 5); + $pdf->Cell(60, 5, 'Le ' . $dateFacture->format('d/m/Y')); + + $pdf->Ln(); + $pdf->Ln(); + $pdf->Ln(); + + $pdf->SetFont('Arial', 'BU', 10); + $pdf->Cell(130, 5, 'Objet : Facture n°' . $cotisation->getInvoiceNumber()); + $pdf->SetFont('Arial', '', 10); + + $pdf->Ln(10); + $pdf->MultiCell(130, 5, $invoiceData->recipient . "\n" . $invoiceData->address . "\n" . $invoiceData->zipcode . "\n" . $invoiceData->city); + + if ($cotisation->getClientReference() !== null) { + $pdf->Ln(10); + $pdf->MultiCell(180, 5, sprintf( + "Référence client : %s", + $cotisation->getClientReference(), + )); + } + + $pdf->Ln(15); + $pdf->MultiCell(180, 5, "Facture concernant votre adhésion à l'Association Française des Utilisateurs de PHP (AFUP)."); + + $dateFin = $cotisation->getEndDate()->getTimestamp(); + $montant = $cotisation->getAmount(); + + if (false === $isSubjectedToVat) { + $pdf->Ln(10); + $pdf->SetFillColor(200, 200, 200); + $pdf->Cell(50, 5, 'Code', 1, 0, 'L', 1); + $pdf->Cell(100, 5, 'Désignation', 1, 0, 'L', 1); + $pdf->Cell(40, 5, 'Prix', 1, 0, 'L', 1); + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(50, 5, 'ADH', 1); + $pdf->Cell(100, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', $dateFin), 1); + $pdf->Cell(40, 5, number_format($montant, 2, '.', '') . ' €', 1); + + $pdf->Ln(15); + $pdf->Cell(10, 5, 'TVA non applicable - art. 293B du CGI'); + } else { + // On stocke le montant de la cotisation TTC. Pour les personnes morales, on extrait le HT pour éviter d'appliquer deux fois la TVA. + if ($cotisation->getUserType() === MemberType::MemberCompany) { + $montant = Vat::getRoundedWithoutVatPriceFromPriceWithVat($montant, Utils::MEMBERSHIP_FEE_VAT_RATE); + } + + $pdf->Ln(10); + $pdf->SetFillColor(200, 200, 200); + $pdf->Cell(20, 5, 'Code', 1, 0, 'L', 1); + $pdf->Cell(95, 5, 'Désignation', 1, 0, 'L', 1); + $pdf->Cell(25, 5, 'Prix HT', 1, 0, 'R', 1); + $pdf->Cell(25, 5, 'Taux TVA', 1, 0, 'R', 1); + $pdf->Cell(25, 5, 'Prix TTC', 1, 0, 'R', 1); + + if ($cotisation->getUserType() === MemberType::MemberCompany) { + [$totalHt, $total] = $this->buildDetailsPersonneMorale($pdf, $montant, $dateFin); + } else { + [$totalHt, $total] = $this->buildDetailsPersonnePhysique($pdf, $montant, $dateFin); + } + + $pdf->Ln(); + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(165, 5, 'Total HT', 1, 0, 'R', 1); + $pdf->Cell(25, 5, $this->formatFactureValue($totalHt) . ' €', 1, 0, 'R', 1); + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(165, 5, 'Total TVA 20%', 1, 0, 'R', 1); + $pdf->Cell(25, 5, $this->formatFactureValue($total - $totalHt) . ' €', 1, 0, 'R', 1); + + $pdf->Ln(); + $pdf->SetFillColor(225, 225, 225); + $pdf->Cell(165, 5, 'Total TTC', 1, 0, 'R', 1); + $pdf->Cell(25, 5, $this->formatFactureValue($total) . ' €', 1, 0, 'R', 1); + } + + $pdf->Ln(15); + $pdf->Cell(10, 5, 'Lors de votre règlement, merci de préciser la mention : "Facture n°' . $cotisation->getInvoiceNumber() . '"'); + + if (is_null($chemin)) { + $pattern = str_replace(' ', '', $invoiceData->patternPrefix) . '_' . $cotisation->getInvoiceNumber() . '_' . date('dmY', $cotisation->getStartDate()->getTimestamp()) . '.pdf'; + $pdf->Output($pattern, 'D', true); + } else { + $pdf->Output($chemin, 'F', true); + } + + return $cotisation->getInvoiceNumber(); + } + + private function buildDetailsPersonneMorale(PDF_Facture $pdf, float $montant, int $dateFin): array + { + $montantTtc = $montant * (1 + Utils::MEMBERSHIP_FEE_VAT_RATE); + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(20, 5, 'ADH', 1); + $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', $dateFin), 1); + $pdf->Cell(25, 5, $this->formatFactureValue($montant) . ' €', 1, 0, 'R'); + $pdf->Cell(25, 5, (Utils::MEMBERSHIP_FEE_VAT_RATE * 100 . ' %'), 1, 0, 'R'); + $pdf->Cell(25, 5, $this->formatFactureValue($montantTtc) . ' €', 1, 0, 'R'); + + return [$montant, $montantTtc]; + } + + private function buildDetailsPersonnePhysique(PDF_Facture $pdf, float $montant, int $dateFin): array + { + $montantFixeHt = 5 / 100 * $montant; + $montantFixeTtc = $montantFixeHt * (1 + Utils::MEMBERSHIP_FEE_VAT_RATE); + $montantVariable = $montant - $montantFixeTtc; + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(20, 5, 'ADH-var', 1); + $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', $dateFin) . ' - part variable', 1); + $pdf->Cell(25, 5, $this->formatFactureValue($montantFixeHt) . ' €', 1, 0, 'R'); + $pdf->Cell(25, 5, (Utils::MEMBERSHIP_FEE_VAT_RATE * 100 . ' %'), 1, 0, 'R'); + $pdf->Cell(25, 5, $this->formatFactureValue($montantFixeTtc) . ' €', 1, 0, 'R'); + + $pdf->Ln(); + $pdf->SetFillColor(255, 255, 255); + $pdf->Cell(20, 5, 'ADH-fixe', 1); + $pdf->Cell(95, 5, "Adhésion AFUP jusqu'au " . date('d/m/Y', $dateFin) . ' - part fixe', 1); + $pdf->Cell(25, 5, $this->formatFactureValue($montantVariable) . ' €', 1, 0, 'R'); + $pdf->Cell(25, 5, '0 %', 1, 0, 'R'); + $pdf->Cell(25, 5, $this->formatFactureValue($montantVariable) . ' €', 1, 0, 'R'); + + return [$montantFixeHt + $montantVariable, $montantFixeTtc + $montantVariable]; + } + + private function formatFactureValue(float $value): string + { + return number_format($value, 2, ',', ' '); + } +} diff --git a/sources/AppBundle/MembershipFee/MembershipFeeMailer.php b/sources/AppBundle/MembershipFee/MembershipFeeMailer.php new file mode 100644 index 000000000..e73974b15 --- /dev/null +++ b/sources/AppBundle/MembershipFee/MembershipFeeMailer.php @@ -0,0 +1,134 @@ +membershipFeeRepository->get($idCotisation); + + if ($membership->getUserType() === MemberType::MemberCompany) { + $company = $this->companyMemberRepository->get($membership->getUserId()); + Assert::notNull($company); + $contactPhysique = [ + 'nom' => $company->getLastName(), + 'prenom' => $company->getFirstName(), + 'email' => $company->getEmail(), + ]; + } else { + $user = $this->userRepository->get($membership->getUserId()); + Assert::notNull($user); + $contactPhysique = [ + 'nom' => $user->getLastName(), + 'prenom' => $user->getFirstName(), + 'email' => $user->getEmail(), + ]; + } + $patternPrefix = $contactPhysique['nom']; + + $corps = "Bonjour,
"; + $corps .= "

Veuillez trouver ci-joint la facture correspondant à votre adhésion à l'AFUP.

"; + $corps .= "

Nous restons à votre disposition pour toute demande complémentaire.

"; + $corps .= "

Le bureau

"; + $corps .= AFUP_RAISON_SOCIALE . "
"; + $corps .= AFUP_ADRESSE . "
"; + $corps .= AFUP_CODE_POSTAL . " " . AFUP_VILLE . "
"; + + $cheminFacture = AFUP_CHEMIN_RACINE . 'cache/fact' . $idCotisation . '.pdf'; + $numeroFacture = $this->pdfGenerator->genererFacture($idCotisation, $cheminFacture); + $pattern = str_replace(' ', '', $patternPrefix) . '_' . $numeroFacture . '_' . date('dmY', $membership->getStartDate()->getTimestamp()) . '.pdf'; + + $message = new Message('Facture AFUP', null, new MailUser( + $contactPhysique['email'], + sprintf('%s %s', $contactPhysique['prenom'], $contactPhysique['nom']), + )); + $message->addAttachment(new Attachment( + $cheminFacture, + $pattern, + 'base64', + 'application/pdf', + )); + $ok = $this->mailer->sendTransactional($message, $corps); + @unlink($cheminFacture); + + return $ok; + } + + public function notifierReglementEnLigneAuTresorier(string $cmd, float $total, string $autorisation, string $transaction): bool + { + if (str_starts_with($cmd, 'F')) { + // Facture + $invoiceNumber = substr($cmd, 1); + $cotisation = $this->membershipFeeRepository->getOneBy(['invoiceNumber' => $invoiceNumber]); + $typePersonne = $cotisation->getUserType()->value; + $idPersonne = $cotisation->getUserId(); + } else { + // Cotisation + [$ref, $date, $typePersonne, $idPersonne, $reste] = explode('-', $cmd, 5); + } + + $infos = [ + 'id' => $idPersonne, + 'type' => $typePersonne, + 'nom' => 'N.C.', + 'prenom' => 'N.C.', + 'email' => 'N.C.', + ]; + + if ($typePersonne == MemberType::MemberCompany->value) { + if ($company = $this->companyMemberRepository->get($idPersonne)) { + $infos['nom'] = $company->getLastName(); + $infos['prenom'] = $company->getFirstName(); + $infos['email'] = $company->getEmail(); + } + } else { + if ($user = $this->userRepository->get($idPersonne)) { + $infos['nom'] = $user->getLastName(); + $infos['prenom'] = $user->getFirstName(); + $infos['email'] = $user->getEmail(); + } + } + + $sujet = "Paiement cotisation AFUP"; + + $corps = "Bonjour, \n\n"; + $corps .= "Une cotisation annuelle AFUP a été réglée.\n\n"; + $corps .= "Personne : " . $infos['nom'] . " " . $infos['prenom'] . " (" . $infos['email'] . ")\n"; + $corps .= "URL : /admin/accounting/membership-fee/list/" . $infos['type'] . "/" . $infos['id'] . "\n"; + $corps .= "Commande : " . $cmd . "\n"; + $corps .= "Total : " . $total . "\n"; + $corps .= "Autorisation : " . $autorisation . "\n"; + $corps .= "Transaction : " . $transaction . "\n\n"; + + $message = new Message($sujet, new MailUser(MailUser::DEFAULT_SENDER_EMAIL, MailUser::DEFAULT_SENDER_NAME), MailUserFactory::tresorier()); + return $this->mailer->sendTransactional($message, $corps); + } +} diff --git a/sources/AppBundle/MembershipFee/MembershipFeeService.php b/sources/AppBundle/MembershipFee/MembershipFeeService.php new file mode 100644 index 000000000..71bf60f91 --- /dev/null +++ b/sources/AppBundle/MembershipFee/MembershipFeeService.php @@ -0,0 +1,142 @@ +setUserType($typePersonne) + ->setUserId($idPersonne) + ->setAmount($montant) + ->setPaymentType($typeReglement !== null ? MembershipFeePayment::from($typeReglement) : null) + ->setPaymentDetails($informationsReglement) + ->setStartDate(new DateTime('@' . $dateDebut)) + ->setEndDate(new DateTime('@' . $dateFin)) + ->setInvoiceNumber($this->membershipFeeRepository->generateInvoiceNumber()) + ->setToken(base64_encode(random_bytes(30))) + ->setComments($commentaires) + ->setClientReference($referenceClient) + ->setInvoiceDate(new \DateTimeImmutable()) + ; + $this->membershipFeeRepository->save($membershipFee); + return true; + } catch (\Exception $e) { + return false; + } + } + + public function isAlreadyPaid(string $cmd): bool + { + return $this->membershipFeeRepository->getOneBy(['paymentDetails' => $cmd]) instanceof MembershipFee; + } + + /** + * Supprime une cotisation + * + * @param $id Identifiant de la cotisation à supprimer + * @return bool Succès de la suppression + */ + public function supprimer(int $id): bool + { + try { + $cotisation = $this->membershipFeeRepository->get($id); + $this->membershipFeeRepository->delete($cotisation); + return true; + } catch (\Exception $exception) { + return false; + } + } + + /** + * Modifie une cotisation + * + * @param $id Identifiant de la cotisation à modifier + * @param $typeReglement Type de règlement (espèces, chèque, virement) + * @param $informationsReglement Informations concernant le règlement (numéro de chèque, de virement etc.) + * @return bool Succès de la modification + */ + public function updatePayment(int $id, int $typeReglement, string $informationsReglement): bool + { + return $this->membershipFeeRepository->updatePayment($id, $typeReglement, $informationsReglement) !== false; + } + + /** + * Retourne la dernière cotisation d'une personne + */ + public function getLatestByUserTypeAndId(MemberType $typePersonne, int $idPersonne): ?MembershipFee + { + return $this->membershipFeeRepository->getLatestByUserTypeAndId($typePersonne, $idPersonne); + } + + public function getNextSubscriptionExpiration(?MembershipFee $cotisation = null): DateTime + { + $endSubscription = $cotisation ? $cotisation->getEndDate() : new DateTime(); + $base = $now = new DateTime(); + + $year = new DateInterval('P1Y'); + + if ($endSubscription > $now) { + $base = $endSubscription; + } + + $base->add($year); + return $base; + } + + /** + * Renvoit la cotisation demandée + * + * @param $invoiceId Identifiant de la facture + * @param $token Token de la facture. Si null, pas de vérification + */ + public function getByInvoice(string $invoiceId, string $token = null): ?MembershipFee + { + $criterias = ['invoiceNumber' => $invoiceId]; + if ($token !== null) { + $criterias['token'] = $token; + } + return $this->membershipFeeRepository->getOneBy($criterias); + } +} diff --git a/sources/AppBundle/MembershipFee/Model/MembershipFee.php b/sources/AppBundle/MembershipFee/Model/MembershipFee.php index ada997107..20f132ad7 100644 --- a/sources/AppBundle/MembershipFee/Model/MembershipFee.php +++ b/sources/AppBundle/MembershipFee/Model/MembershipFee.php @@ -31,6 +31,8 @@ class MembershipFee implements NotifyPropertyInterface private ?int $nbReminders = null; private ?DateTime $lastReminderDate = null; + private bool $downloadInvoice = false; + public function getId(): ?int { return $this->id; @@ -210,4 +212,14 @@ public function setLastReminderDate(?DateTime $lastReminderDate): self $this->lastReminderDate = $lastReminderDate; return $this; } + + public function canDownloadInvoice(): bool + { + return $this->downloadInvoice; + } + + public function setDownloadInvoice(bool $hasPermission): bool + { + return $this->downloadInvoice = $hasPermission; + } } diff --git a/sources/AppBundle/MembershipFee/Model/Repository/MembershipFeeRepository.php b/sources/AppBundle/MembershipFee/Model/Repository/MembershipFeeRepository.php index bed0966f2..8f7b4d7d3 100644 --- a/sources/AppBundle/MembershipFee/Model/Repository/MembershipFeeRepository.php +++ b/sources/AppBundle/MembershipFee/Model/Repository/MembershipFeeRepository.php @@ -8,6 +8,8 @@ use AppBundle\Controller\Admin\Membership\MembershipFeePayment; use AppBundle\MembershipFee\Model\MembershipFee; use Aura\SqlQuery\Common\SelectInterface; +use CCMBenchmark\Ting\Repository\Collection; +use CCMBenchmark\Ting\Repository\HydratorSingleObject; use CCMBenchmark\Ting\Repository\Metadata; use CCMBenchmark\Ting\Repository\MetadataInitializer; use CCMBenchmark\Ting\Repository\Repository; @@ -15,6 +17,9 @@ use CCMBenchmark\Ting\Serializer\SerializerFactoryInterface; use DateTime; +/** + * @extends Repository + */ class MembershipFeeRepository extends Repository implements MetadataInitializer { public function getMembershipStartingDate(MemberType $memberType, int $idMember): DateTime @@ -53,6 +58,55 @@ public function generateInvoiceNumber(): string return 'COTIS-' . date('Y') . '-' . (is_null($result?->number) ? 1 : $result->number); } + public function updatePayment(int $id, int $paymentType, string $paymentInfos): bool + { + $sql = 'UPDATE'; + $sql .= ' afup_cotisations '; + $sql .= 'SET'; + $sql .= ' type_reglement= :paymentType,'; + $sql .= ' informations_reglement=:paymentInfos'; + $sql .= ' WHERE'; + $sql .= ' id=' . $id; + + return $this->getQuery($sql)->setParams(['paymentType' => $paymentType, 'paymentInfos' => $paymentInfos, 'id' => $id])->execute(); + } + + public function getLatestByUserTypeAndId(MemberType $type_personne, int $id_personne): ?MembershipFee + { + $sql = 'SELECT'; + $sql .= ' * '; + $sql .= 'FROM'; + $sql .= ' afup_cotisations '; + $sql .= 'WHERE'; + $sql .= ' type_personne=:userType '; + $sql .= ' AND id_personne=:userId '; + $sql .= 'ORDER BY'; + $sql .= ' date_fin DESC '; + $sql .= 'LIMIT 0, 1 '; + + $collection = $this->getQuery($sql)->setParams(['userType' => $type_personne->value, 'userId' => $id_personne])->query($this->getCollection(new HydratorSingleObject())); + if ($collection->count() === 0) { + return null; + } + return $collection->first(); + } + + /** + * @return Collection + */ + public function getListByUserTypeAndId(MemberType $memberType, int $memberId): Collection + { + $sql = 'SELECT * '; + $sql .= 'FROM'; + $sql .= ' afup_cotisations '; + $sql .= 'WHERE'; + $sql .= ' type_personne=:userType '; + $sql .= ' AND id_personne=:userId '; + $sql .= 'ORDER BY date_fin DESC'; + + return $this->getQuery($sql)->setParams(['userType' => $memberType->value, 'userId' => $memberId])->query($this->getCollection(new HydratorSingleObject())); + } + public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) { $metadata = new Metadata($serializerFactory); diff --git a/sources/AppBundle/MembershipFee/OnlinePaymentHandler.php b/sources/AppBundle/MembershipFee/OnlinePaymentHandler.php new file mode 100644 index 000000000..3ce29b933 --- /dev/null +++ b/sources/AppBundle/MembershipFee/OnlinePaymentHandler.php @@ -0,0 +1,75 @@ + MemberType::MemberCompany->value, 'id' => (int) $arr[2]]; + } + + // Depuis une cotisation : $cmd=C2023-211120232237-0-5-PAUL-431 + [$ref, $date, $memberType, $memberId, $stuff] = $arr; + + return ['type' => (int) $memberType, 'id' => (int) $memberId]; + } + + public function validerReglementEnLigne(string $cmd, float $total, string $autorisation, string $transaction): mixed + { + $reference = substr($cmd, 0, strlen($cmd) - 4); + $verif = substr($cmd, strlen($cmd) - 3, strlen($cmd)); + $result = false; + + if (str_starts_with($cmd, 'F')) { + // This is an invoice ==> we dont have to create a new cotisation, just update the existing one + $invoiceNumber = substr($cmd, 1); + $cotisation = $this->membershipFeeService->getByInvoice($invoiceNumber); + + $this->membershipFeeService->updatePayment( + $cotisation->getId(), + AFUP_COTISATIONS_REGLEMENT_ENLIGNE, + "autorisation : " . $autorisation . " / transaction : " . $transaction, + ); + } elseif (substr(md5($reference), -3) === strtolower($verif) && !$this->membershipFeeService->isAlreadyPaid($cmd)) { + [$ref, $date, $typePersonne, $idPersonne, $reste] = explode('-', $cmd, 5); + $dateDebut = mktime(0, 0, 0, (int) substr($date, 2, 2), (int) substr($date, 0, 2), (int) substr($date, 4, 4)); + + $cotisation = $this->membershipFeeService->getLatestByUserTypeAndId(MemberType::from((int) $typePersonne), (int) $idPersonne); + $dateFinPrecedente = !$cotisation instanceof MembershipFee ? 0 : $cotisation->getEndDate()->getTimestamp(); + + if ($dateFinPrecedente > 0) { + $dateDebut = strtotime('+1day', $dateFinPrecedente); + } + + $dateFin = $this->membershipFeeService->getNextSubscriptionExpiration($cotisation)->getTimestamp(); + $result = $this->membershipFeeService->ajouter( + MemberType::from((int) $typePersonne), + (int) $idPersonne, + $total, + AFUP_COTISATIONS_REGLEMENT_ENLIGNE, + $cmd, + $dateDebut, + $dateFin, + "autorisation : " . $autorisation . " / transaction : " . $transaction, + ); + } + + return $result; + } +} diff --git a/sources/AppBundle/Security/MembershipFeeVoter.php b/sources/AppBundle/Security/MembershipFeeVoter.php new file mode 100644 index 000000000..cdd7c57a0 --- /dev/null +++ b/sources/AppBundle/Security/MembershipFeeVoter.php @@ -0,0 +1,46 @@ +membershipFeeRepository->get((int) $subject); + if (!$cotisation instanceof MembershipFee) { + return false; + } + + if ($cotisation->getUserType() === MemberType::MemberPhysical) { + return $cotisation->getUserId() === $this->droits->obtenirIdentifiant(); + } + + if ($cotisation->getUserType() === MemberType::MemberCompany) { + return $this->droits->verifierDroitManagerPersonneMorale($cotisation->getUserId()); + } + + return false; + } +} diff --git a/templates/admin/association/membership/membershipfee.html.twig b/templates/admin/association/membership/membershipfee.html.twig index 3a278063f..2bead8f7a 100644 --- a/templates/admin/association/membership/membershipfee.html.twig +++ b/templates/admin/association/membership/membershipfee.html.twig @@ -38,18 +38,18 @@ {% for cotisation in cotisations %} - {{ cotisation.date_debut|date('d/m/Y') }} + {{ cotisation.startDate|date('d/m/Y') }} - {{ cotisation.date_fin|date('d/m/Y') }} - {% if cotisation.date_debut < time and cotisation.date_fin > time %} + {{ cotisation.endDate|date('d/m/Y') }} + {% if cotisation.startDate < time and cotisation.endDate > time %} (toujours valide) {% endif %} - {{ cotisation.montant|number_format(2, ',') }} € + {{ cotisation.amount|number_format(2, ',') }} € - {% if cotisation.telecharger_facture %} - + {% if cotisation.canDownloadInvoice %} + Télécharger la facture diff --git a/templates/site/company_membership/payment.html.twig b/templates/site/company_membership/payment.html.twig index fea0560e5..3bd992b88 100644 --- a/templates/site/company_membership/payment.html.twig +++ b/templates/site/company_membership/payment.html.twig @@ -7,17 +7,17 @@ Votre inscription a bien été enregistrée ! Il ne vous reste plus qu'à régler votre cotisation.

- Montant de la cotisation: {{ invoice.montant }} Euros + Montant de la cotisation: {{ invoice.amount|number_format(2, '.', ' ') }} Euros

Facture disponible

- Télécharger la facture + Télécharger la facture

Régler votre cotisation par Carte Bleue

{{ paybox|raw }}

Régler votre cotisation par chèque

-

Libellez le chèque à cet ordre: {{ afup.raison_sociale }}. Indiquez au dos le numéro de facture: {{ invoice.numero_facture }}

+

Libellez le chèque à cet ordre: {{ afup.raison_sociale }}. Indiquez au dos le numéro de facture: {{ invoice.invoiceNumber }}

{{ afup.raison_sociale }}
{{ afup.adresse }}
@@ -25,7 +25,7 @@

Régler votre cotisation par virement bancaire

- Lors d'un règlement par virement bancaire, indiquez ce libellé: {{ invoice.numero_facture }}. + Lors d'un règlement par virement bancaire, indiquez ce libellé: {{ invoice.invoiceNumber }}.

diff --git a/tests/unit/Afup/Association/CotisationsTest.php b/tests/unit/AppBundle/MembershipFee/MembershipFeeServiceTest.php similarity index 69% rename from tests/unit/Afup/Association/CotisationsTest.php rename to tests/unit/AppBundle/MembershipFee/MembershipFeeServiceTest.php index 2a0712941..0721883cc 100644 --- a/tests/unit/Afup/Association/CotisationsTest.php +++ b/tests/unit/AppBundle/MembershipFee/MembershipFeeServiceTest.php @@ -2,15 +2,17 @@ declare(strict_types=1); -namespace Afup\Site\Tests\Association; +namespace AppBundle\Tests\MembershipFee; -use Afup\Site\Association\Cotisations; -use Afup\Site\Utils\Base_De_Donnees; use AppBundle\Association\MemberType; +use AppBundle\MembershipFee\MembershipFeeService; +use AppBundle\MembershipFee\Model\MembershipFee; +use AppBundle\MembershipFee\Model\Repository\MembershipFeeRepository; +use AppBundle\MembershipFee\OnlinePaymentHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -final class CotisationsTest extends TestCase +final class MembershipFeeServiceTest extends TestCase { public static function generateCotisationProvider(): array { @@ -37,11 +39,14 @@ public static function generateCotisationProvider(): array #[DataProvider('generateCotisationProvider')] public function testFinProchaineCotisation(\DateTimeInterface $dateFin, \DateTimeInterface $expected): void { - $bdd = new Base_De_Donnees('', '', '', ''); + $membershipFeeRepository = $this->createMock(MembershipFeeRepository::class); - $cotisations = new Cotisations($bdd); + $membershipFeeService = new MembershipFeeService($membershipFeeRepository); - $actual = $cotisations->finProchaineCotisation(['date_fin' => $dateFin->format('U')]); + $membershipFee = new MembershipFee(); + $membershipFee->setEndDate(new \DateTime('@' . $dateFin->format('U'))); + + $actual = $membershipFeeService->getNextSubscriptionExpiration($membershipFee); self::assertEquals($expected->format('Y-m-d'), $actual->format('Y-m-d')); } @@ -63,11 +68,13 @@ public static function accountCmdProvider(): array #[DataProvider('accountCmdProvider')] public function testGetAccountFromCmd(string $cmd, array $expected): void { - $bdd = new Base_De_Donnees('', '', '', ''); + $membershipFeeRepository = $this->createMock(MembershipFeeRepository::class); - $cotisations = new Cotisations($bdd); + $membershipFeeService = new MembershipFeeService($membershipFeeRepository); - $actual = $cotisations->getAccountFromCmd($cmd); + $actual = (new OnlinePaymentHandler( + $membershipFeeService, + ))->getAccountFromCmd($cmd); self::assertEquals($expected, $actual); }