<?php
namespace Evo\Infrastructure\DoctrineSubscriber;
use App\Enum\ActivityLogCategoryEnum;
use App\Enum\InvoiceStatusEnum;
use App\Enum\InvoiceTypeEnum;
use App\Enum\OrganizationStatusEnum;
use App\Enum\OrganizationStatusPayedEnum;
use App\Enum\PaymentStatusEnum;
use App\Enum\PaymentTypeEnum;
use App\Enum\ProductKeyEnum;
use App\Enum\QuoteStatusEnum;
use App\Repository\UserRepository;
use App\Service\Invoices\InvoiceService;
use App\Service\OrganizationUtils;
use App\Service\SegmentAPI;
use App\Traits\SentryNotifyTrait;
use App\Utils\InvoiceUtils;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Evo\Infrastructure\MappingORM\ActivityLog;
use Evo\Infrastructure\MappingORM\DiscountCode;
use Evo\Infrastructure\MappingORM\Invoice;
use Evo\Infrastructure\MappingORM\Organization;
use Evo\Infrastructure\MappingORM\Payment;
use Evo\Infrastructure\MappingORM\PostalAddress;
use Evo\Infrastructure\MappingORM\Product;
use Evo\Infrastructure\MappingORM\PromoCodeHistory;
use Evo\Infrastructure\MappingORM\User;
use Evo\Infrastructure\Messenger\Message\SyncBillingReportMessage;
use Evo\Infrastructure\Repository\InvoiceRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsFifoStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class InvoiceSubscriber implements EventSubscriber
{
use SentryNotifyTrait;
private EntityManagerInterface $em;
private OrganizationUtils $organizationUtils;
private SegmentAPI $segmentAPI;
private InvoiceUtils $invoiceUtils;
private HttpClientInterface $client;
private Security $security;
private HubInterface $hub;
private InvoiceRepository $invoiceRepository;
private MessageBusInterface $bus;
private RequestStack $request;
private InvoiceService $invoiceService;
public function __construct(
OrganizationUtils $organizationUtils,
SegmentAPI $segmentAPI,
InvoiceUtils $invoiceUtils,
HttpClientInterface $client,
Security $security,
EntityManagerInterface $em,
HubInterface $hub,
InvoiceRepository $invoiceRepository,
RequestStack $request,
MessageBusInterface $bus,
InvoiceService $invoiceService
) {
$this->organizationUtils = $organizationUtils;
$this->segmentAPI = $segmentAPI;
$this->invoiceUtils = $invoiceUtils;
$this->client = $client;
$this->em = $em;
$this->security = $security;
$this->hub = $hub;
$this->invoiceRepository = $invoiceRepository;
$this->request = $request;
$this->bus = $bus;
$this->invoiceService = $invoiceService;
}
public function getSubscribedEvents(): array
{
return [
'preUpdate',
'postUpdate',
'prePersist',
'postPersist',
'preRemove',
];
}
/**
* @throws TransportExceptionInterface
*/
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Invoice) {
return;
}
$quote = $entity->getQuote();
if (InvoiceStatusEnum::PAID === $entity->getStatus() && ($quote && QuoteStatusEnum::VALID !== $quote->getStatus())) {
$quote->setStatus(QuoteStatusEnum::VALID);
$this->em->persist($quote);
$this->em->flush();
}
if (in_array($entity->getStatus(), [InvoiceStatusEnum::SENT, InvoiceStatusEnum::PARTIALLY_PAID, InvoiceStatusEnum::PAID], true)) {
$this->invoiceUtils->generateInvoice($entity);
}
$request = $this->request->getCurrentRequest();
if ($request instanceof Request) {
$data = json_decode($request->getContent(), true);
$isAccountsSubmission = $data['isAccountsSubmission'] ?? $request->get('isAccountsSubmission');
if ($isAccountsSubmission) {
$payment = new Payment();
$payment->setType(PaymentTypeEnum::GOCARDLESS);
$payment->setPaidOutAt(new \DateTime($entity->getCreatedAt()->format('Y-m-d H:s:i')));
$payment->setAmount($entity->getPrice());
$payment->setInvoice($entity);
$entity->addPayment($payment);
$payment->setStatus(PaymentStatusEnum::WAITING);
$this->em->persist($payment);
$this->em->flush();
}
}
$this->invoiceUtils->generatePaymentGocardless($entity->getPrice(), $entity);
$this->createOrganizationPrescriber($entity);
$organization = $entity->getOrganization();
if (null !== $organization) {
$items = $entity->getItems();
foreach ($items as $itemPerInvoice) {
$product = $itemPerInvoice->getProduct();
if (ProductKeyEnum::IMMATRICULATION === $product->getUniqueKey() && !$organization->getIsNewImmatriculation()) {
$organization->setIsNewImmatriculation(true);
$this->em->persist($organization);
$this->em->flush();
}
}
if (InvoiceStatusEnum::PAID === $entity->getStatus()) {
$status = $this->organizationUtils->checkStatusDomiciliation($organization);
if ($status) {
$organization->setStatus($status);
$this->em->persist($organization);
$this->em->flush();
}
}
if (null !== $organization->getId()) {
$this->invoiceUtils->checkUnpaid($organization);
}
}
if ($entity->isModel()) {
return;
}
/*
* Track segment
*/
$this->segmentAPI->identifyNewInvoice($entity);
$this->segmentAPI->trackInvoiceStatus($entity);
$this->segmentAPI->trackInvoiceCreated($entity);
$this->segmentAPI->trackInvoicePaid($entity);
$this->segmentAPI->trackError($entity);
try {
$this->handleZapierHook($entity->getId(), $organization);
} catch (\Exception $e) {
$this->sendSentryMessage($e->getMessage());
}
$duplicatedInvoices = $this->invoiceRepository->getDuplicatedInvoice(null, true);
if (\count($duplicatedInvoices) > 0) {
$update = new Update(
'http://api.digidom.pro/signal/check/duplicated/invoice',
json_encode([
'status' => 'duplicated',
], JSON_THROW_ON_ERROR)
);
$this->hub->publish($update);
}
try {
$this->invoiceService->processInvoiceReactivationFee($entity);
} catch (\Exception $e) {
$this->sendSentryMessage($e->getMessage());
}
try {
if (null !== $entity->getNumber()) {
$this->bus->dispatch((new Envelope(
new SyncBillingReportMessage($entity->getId())
))->with(
new AmazonSqsFifoStamp('SyncBillingReport', uniqid('', true))
));
}
} catch (\Exception $e) {
$this->sendSentryMessage($e->getMessage());
}
// create subscription if not exist
$this->invoiceService->createIfMissingFromInvoice($entity);
}
public function preRemove(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Invoice) {
return;
}
if ($entity->isModel()) {
return;
}
$user = $this->security->getUser();
$organization = $entity->getOrganization();
$activityLog = new ActivityLog();
$activityLog->setOrganization($organization);
$user instanceof User ? $activityLog->setUser($user) : $activityLog->setUser(null);
$activityLog->setObject('SUPPRESSION FACTURE');
$activityLog->setMethod('CREATION');
$activityLog->setCategory(ActivityLogCategoryEnum::INCIDENT);
$activityLog->setNote($entity->getName());
$this->em->persist($activityLog);
$this->em->flush();
}
public function postUpdate(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Invoice) {
return;
}
$uow = $this->em->getUnitOfWork();
$uow->computeChangeSets();
$changeset = $uow->getEntityChangeSet($entity);
if (1 === count($changeset) && isset($changeset['updatedAt'])) {
return;
}
if (2 === count($changeset) && isset($changeset['reference'])) {
return;
}
if (2 === count($changeset) && isset($changeset['invoiceType'])) {
return;
}
if ((isset($changeset['path'], $changeset['updatedAt']) && 2 === count($changeset)) || isset($changeset['isImported'])) {
return;
}
if (2 === count($changeset) && isset($changeset['isDomiciliation'])) {
return;
}
$organization = $entity->getOrganization();
if (isset($changeset['number'])) {
try {
$this->invoiceUtils->generateInvoice($entity);
} catch (\Exception $e) {
$this->sendSentryMessage($e->getMessage());
}
}
if (
isset($changeset['discountCode'])
&& $changeset['discountCode'][1] &&
$organization
) {
/** @var DiscountCode $discountCode */
$discountCode = $changeset['discountCode'][1];
$promoCodeHistory = new PromoCodeHistory();
$promoCodeHistory->addDiscountCode($discountCode);
$promoCodeHistory->setPromoCode($discountCode->getPromoCode());
$promoCodeHistory->setDiscountCodeType($discountCode->getType());
$promoCodeHistory->setOrganizationId($organization->getId());
$promoCodeHistory->setAmount($discountCode->getFirstDiscountConfiguration()->getAmount());
$promoCodeHistory->setInvoice($entity);
$this->em->persist($promoCodeHistory);
$this->em->flush();
}
if (isset($changeset['status']) && InvoiceStatusEnum::PAID === $changeset['status'][1] && ($organization && OrganizationStatusPayedEnum::PAID === $organization->getStatusInvoicePayed() && false === $organization->getIsAuthorizedDebit())) {
$organization->setIsAuthorizedDebit(true);
$this->em->persist($organization);
$this->em->flush();
}
$this->setInvoiceType($entity);
if (isset($changeset['status']) && $organization) {
// check if organization status is LOST and the invoice is the first invoice of the organization with status PAID
$firstInvoiceId = $this->invoiceRepository->getFirstInvoiceIdByOrganization($organization->getId());
if (OrganizationStatusEnum::LOST === $organization->getStatus()
&& $entity->getId() === $firstInvoiceId
&& InvoiceStatusEnum::PAID === $changeset['status'][1]) {
// set the domiciliation start date before checkUnpaid
$organization->setDomiciliationStartDate(new \DateTime());
$organization->setStatus(OrganizationStatusEnum::NEW_PAYMENT);
}
// checkUnpaid && flush organization
$this->invoiceUtils->checkUnpaid($organization);
if (InvoiceStatusEnum::LATE === $changeset['status'][1]) {
$this->segmentAPI->identifyInvoicesOrganization($organization);
}
}
$status = null;
if (!$entity->getIsPackInvoice() && $organization) {
$status = $this->organizationUtils->checkStatusDomiciliation($organization);
}
if ($status) {
$organization->setStatus($status);
$this->em->persist($organization);
$this->em->flush();
}
$quote = $entity->getQuote();
if (InvoiceStatusEnum::PAID === $entity->getStatus() && ($quote && QuoteStatusEnum::VALID !== $quote->getStatus())) {
$quote->setStatus(QuoteStatusEnum::VALID);
$this->em->persist($quote);
$this->em->flush();
}
if ($entity->isModel()) {
return;
}
// check status of invoice to change status to organization
$this->segmentAPI->identifyNewInvoice($entity);
$this->segmentAPI->trackError($entity);
if (isset($changeset['status'])) {
$this->segmentAPI->trackInvoiceStatus($entity);
/*
* Track invoice PAID
*/
if (InvoiceStatusEnum::PAID === $changeset['status'][1] && $entity->getOrganization()) {
$this->segmentAPI->trackInvoicePaid($entity);
}
}
$cancelled = false;
if ($entity->getCreditNotes()->count() > 0) {
$creditNotes = $entity->getCreditNotes();
$sum = 0;
foreach ($creditNotes as $creditNote) {
$sum += $creditNote->getAmount();
}
// if sum of credit notes is equal to invoice price, set invoice status to rejected
if ($sum === $entity->getPrice()) {
$cancelled = true;
}
}
}
/**
* @throws \Exception
*/
public function preUpdate(PreUpdateEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Invoice) {
return;
}
$uow = $this->em->getUnitOfWork();
$uow->computeChangeSets();
$changeset = $uow->getEntityChangeSet($entity);
if (1 === count($changeset) && isset($changeset['updatedAt'])) {
return;
}
if (isset($changeset['isImported']) || isset($changeset['invoiceType'])) {
return;
}
if (isset($changeset['path'], $changeset['updatedAt'])) {
return;
}
if (2 === count($changeset) && isset($changeset['isDomiciliation'])) {
return;
}
$entity = $this->invoiceUtils->checkInvoice($entity);
$changeSet = $args->getEntityChangeSet();
if (isset($changeset['status']) && !$entity->getPaymentDeadline()) {
$paymentDeadline = date('Y-m-d', strtotime('+1 months', $entity->getCreatedAt()->getTimestamp()));
$entity->setPaymentDeadline(new \DateTime($paymentDeadline));
}
if (!isset($changeset['reference']) && [] !== $changeSet) {
InvoiceUtils::setPrices($entity);
$this->createOrganization($entity);
if (isset($changeSet['isSent']) && true === $changeSet['isSent'][1] && [] !== $entity->getOrganization()->getUsers()) {
$entity->setStatus(InvoiceStatusEnum::SENT);
$entity->setSentDate(new \DateTime());
$organization = $entity->getOrganization();
if ($organization && $organization->getUsers()) {
$entity->setSentTo($organization->getUsers()[0]->getEmail());
}
}
}
}
private function createOrganization(Invoice $entity): void
{
if ($entity->getIsNewOrganization()) {
if (null === $entity->getNewOrganizationAddress()) {
$newAddress = (new PostalAddress())
->setAddressCountry('')
->setAddressLocality('')
->setStreetAddress('')
->setPostalCode('')
->setAddressRegion('');
$entity->setNewOrganizationAddress($newAddress);
}
$organization = (new Organization())
->setLegalName($entity->getNewOrganizationLegalName())
->setVatID($entity->getNewOrganizationVATID())
->setAddress($entity->getNewOrganizationAddress())
->setTelephone($entity->getNewOrganizationPhoneNumber());
/** @var UserRepository $userRepo */
$userRepo = $this->em->getRepository(User::class);
$user = $userRepo->loadUserByUsername($entity->getNewOrganizationUserEmail());
if (null === $user) {
$user = (new User())
->setPhoneNumber($entity->getNewOrganizationPhoneNumber())
->setEmail($entity->getNewOrganizationUserEmail())
->setPassword(uniqid().uniqid().uniqid());
}
/* @phpstan-ignore-next-line */
$organization->addUser($user);
$entity->setOrganization($organization)
->setNewOrganizationAddress(null)
->setNewOrganizationLegalName(null)
->setNewOrganizationVATID(null)
->setIsNewOrganization(false)
->setNewOrganizationUserEmail(null)
->setNewOrganizationPhoneNumber(null);
}
}
private function createOrganizationPrescriber(Invoice $entity): void
{
if (!$entity->getIsNewOrganizationPrescribed()) {
return;
}
if (null === $entity->getPrescriber()) {
if (null === $entity->getNewPrescriberAddress()) {
$newAddress = (new PostalAddress())
->setAddressCountry('')
->setAddressLocality('')
->setStreetAddress('')
->setPostalCode('')
->setAddressRegion('');
$entity->setNewPrescriberAddress($newAddress);
}
$organization = (new Organization())
->setLegalName($entity->getNewPrescriberLegalName())
->setVatID($entity->getNewPrescriberVATID())
->setAddress($entity->getNewPrescriberAddress())
->setIsPrescriber(true);
/** @var UserRepository $userRepo */
$userRepo = $this->em->getRepository(User::class);
$user = $userRepo->loadUserByUsername($entity->getNewPrescriberUserEmail());
if (null === $user) {
$user = (new User())
->setEmail($entity->getNewPrescriberUserEmail())
->setPassword(uniqid().uniqid().uniqid());
}
/** @var Organization $factOrganization */
$factOrganization = $entity->getOrganization();
$factOrganization->setPrescriber($organization);
if ($entity->getIsAttachedToPrescriber()) {
$entity->setOrganization($organization);
}
$this->em->persist($organization);
$this->em->flush();
$this->segmentAPI->identifyPrescriber($factOrganization->getPrescriber());
} else {
$organization = $entity->getOrganization();
if (null !== $organization) {
$organization->setPrescriber($entity->getPrescriber());
$organization->getPrescriber()->addOrganizationsPrescribed($organization);
if ($entity->getIsAttachedToPrescriber()) {
$entity->setOrganization($organization->getPrescriber());
}
$this->em->persist($entity);
$this->em->flush();
$this->segmentAPI->identifyPrescriber($organization->getPrescriber());
}
}
}
/**
* @throws \Exception
*/
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Invoice) {
return;
}
if (InvoiceStatusEnum::DRAFT !== $entity->getStatus() && !$entity->getPaymentDeadline()) {
$paymentDeadline = date('Y-m-d', strtotime('+1 months', $entity->getCreatedAt()->getTimestamp()));
$entity->setPaymentDeadline(new \DateTime($paymentDeadline));
}
if ($entity->getIsDomiciliation()) {
foreach ($entity->getItems() as $item) {
$item->setDescription($item->getDescription());
}
}
$this->createOrganization($entity);
InvoiceUtils::setPrices($entity);
if ($entity->isModel()) {
return;
}
$entity->setStatus(InvoiceStatusEnum::DRAFT);
if ($entity->getIsSent()) {
$entity->setStatus(InvoiceStatusEnum::SENT);
}
$entity = $this->invoiceUtils->checkInvoice($entity);
$user = $this->security->getUser();
if ($user instanceof User) {
$entity->setUser($user);
}
// Init IsNewTransfert for organization
if ($entity->isFlagTransfertToInit($entity)) {
$entity->getOrganization()->setIsNewTransfert(true);
}
}
/** this function set the InvoiceType using the organization, invoices, items and products.
*/
public function setInvoiceType(Invoice $entity): void
{
$key = [
ProductKeyEnum::COLIS_FRAIS_DE_REEXPEDITION,
ProductKeyEnum::COLIS_FRAIS_DE_GARDE,
ProductKeyEnum::COLIS_FRAIS_DE_GESTION,
ProductKeyEnum::FRAIS_DE_REEXPEDITION_ETRANGER,
ProductKeyEnum::REEXPEDITION_COURRIER,
ProductKeyEnum::FRAIS_DE_STOCKAGE,
];
$organization = $entity->getOrganization();
if (!$organization instanceof Organization) {
return;
}
$isNewDomiciliation = $entity->getIsDomiciliation();
/* @var Organization $organization */
if (true === $isNewDomiciliation) {
$entity->setInvoiceType(InvoiceTypeEnum::PERIODICAL);
$this->em->persist($organization);
} elseif (false === $isNewDomiciliation) {
$entity->setInvoiceType(null);
$this->em->persist($entity);
}
$items = $entity->getItems();
foreach ($items as $itemPerInvoice) {
/** @var Product $product */
$product = $itemPerInvoice->getProduct();
if (in_array($product->getUniqueKey(), $key, true)) {
$entity->setInvoiceType(InvoiceTypeEnum::RESHIPPING_COSTS);
$this->em->persist($entity);
} elseif ($product->getCategory() && 'Formalités juridiques' === $product->getCategory()->getTitle()) {
$entity->setInvoiceType(InvoiceTypeEnum::FORMALITY);
$this->em->persist($entity);
} elseif ('frais_de_retard' === $product->getUniqueKey()) {
$entity->setInvoiceType(InvoiceTypeEnum::REJECTED);
$this->em->persist($entity);
}
}
$this->em->flush();
}
/**
* @throws TransportExceptionInterface
*/
private function handleZapierHook(?int $invoiceId, ?Organization $organization): void
{
if ($organization && !empty($organization->getZapierHookUrl())) {
foreach ($organization->getZapierHookUrl() as $zapierHookUrl) {
if (isset($zapierHookUrl['invoice']) && 'invoice' === array_keys($zapierHookUrl)[0]) {
$params = [
'organization' => $organization->getId(),
'invoiceId' => $invoiceId,
];
$response = $this->client->request(
'POST',
$zapierHookUrl['invoice'],
[
'body' => $params,
]
);
if (Response::HTTP_GONE === $response->getStatusCode()) {
$this->sendSentryMessage('[ZAPIER] Unsubscribe Zap 410 returned');
}
}
}
}
}
}