<?php
/**
* This file is part of the CosaVostra, TrackPay, Symfony package.
* (c) Mohamed Radhi GUENNICHI <rg@mate.tn> <+216 50 711 816>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App\Controller\App;
use App\Connector\Driver\Sellsy\Driver;
use App\DTO\PayProcedureDto;
use App\Entity\GoCardlessAccount;
use App\Entity\GoCardlessMandate;
use App\Entity\Invoice;
use App\Entity\Procedure;
use App\Entity\ProcedureStep;
use App\Event\GoCardlessMandateEvent;
use App\Event\InvoicePaymentEvent;
use App\Event\ProcedureClaimEvent;
use App\Event\ProcedureClosedEvent;
use App\Event\ProcedureInvoicePaidEvent;
use App\Event\ProcedureStepNotificationMethodEvent;
use App\Events;
use App\Factory\ProcedureClaimFactory;
use App\Form\App\ConfirmActionType;
use App\Form\App\InvoiceCustomerContactType;
use App\Form\App\PayProcedureType;
use App\Form\App\ProcedureClaimType;
use App\Job\Message\ProceduresDispatch;
use App\Manager\GoCardlessAccountManager;
use App\Manager\ProcedureManager;
use App\Payment\Stripe\Client;
use App\Repository\ConnectorRepository;
use App\Repository\GoCardlessAccountRepository;
use App\Repository\GoCardlessMandateRepository;
use App\Repository\InvoiceRepository;
use App\Repository\ProcedureClaimRepository;
use App\Service\GoCardlessService;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use GoCardlessPro\Core\Exception\InvalidStateException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
/**
* Class PageController
* @package App\Controller\App
* @Route(name="app_page_")
*/
class PageController extends AbstractController
{
/**
* @var EventDispatcherInterface|EventDispatcher
*/
protected $eventDispatcher;
/**
* @var TranslatorInterface|Translator
*/
protected $translator;
/**
* PageController constructor.
*
* @param EventDispatcherInterface $eventDispatcher
* @param TranslatorInterface $translator
*/
public function __construct(EventDispatcherInterface $eventDispatcher, TranslatorInterface $translator)
{
$this->eventDispatcher = $eventDispatcher;
$this->translator = $translator;
}
/**
* @return Response
* @Route("", name="index")
*/
public function index(): Response
{
if (null !== $this->getUser()) {
return $this->redirectToRoute('app_invoice_index');
}
return $this->redirectToRoute('app_security_signin');
}
/**
* @param Procedure $procedure
* @param ProcedureManager $manager
* @param Request $request
*
* @return Response
* @Route("/action/procedures/{closeToken}/close", name="procedure_close")
*/
public function closeProcedure(Procedure $procedure, ProcedureManager $manager, Request $request): Response
{
if (!$procedure->isPending()) {
throw new NotFoundHttpException();
}
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getAccountLocale()
);
if (!$request->query->has('reason')) {
return $this->render('app/close/close.html.twig', [
'invoice' => $invoice,
'token' => $procedure->getCloseToken()
]);
}
$event = new ProcedureClosedEvent($procedure);
$manager->close(
$procedure,
$request->query->get('reason', Procedure::CLOSE_REASON_CANCELLED)
);
$this->eventDispatcher->dispatch($event, Events::PROCEDURE_CLOSED);
return $this->render('app/close/closed.html.twig', [
'invoice' => $invoice
]);
}
/**
* @param Request $request
* @param Procedure $procedure
* @param ProcedureManager $manager
*
* @return Response
* @Route("/action/procedures/{snoozeToken}/snooze", name="procedure_snooze")
*/
public function snoozeProcedure(Request $request, Procedure $procedure, ProcedureManager $manager): Response
{
if (!$procedure->isPending()) {
throw new ConflictHttpException();
}
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getAccountLocale()
);
$form = $this->createForm(ConfirmActionType::class, null, [
'action_label' => 'action.confirm.checkbox.procedure.snooze'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Pause the procedure for 7 days.
$manager->pause($procedure, 7);
return $this->render('app/action/confirmed.html.twig', [
'title' => 'action.confirm.title.snooze',
'invoice' => $invoice,
'result_title' => 'action.result.snooze.title',
'result_text' => 'action.result.snooze.text'
]);
}
return $this->render('app/action/confirm.html.twig', [
'title' => 'action.confirm.title.snooze',
'invoice' => $invoice,
'submit_trans' => 'action.confirm.submit.snooze',
'want_confirm_level1_trans' => 'action.snooze.want.confirm.level1',
'want_confirm_level2_trans' => 'action.snooze.want.confirm.level2',
'form' => $form->createView()
]);
}
/**
* @param Request $request
* @param Procedure $procedure
* @param ProcedureManager $manager
*
* @return Response
* @Route("/action/procedures/{resumeToken}/resume", name="procedure_resume")
*/
public function resumeProcedure(Request $request, Procedure $procedure, ProcedureManager $manager): Response
{
if (!$procedure->isPaused()) {
throw new ConflictHttpException();
}
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getAccountLocale()
);
$form = $this->createForm(ConfirmActionType::class, null, [
'action_label' => 'action.confirm.checkbox.procedure.resume'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$manager->resume($procedure);
return $this->render('app/action/confirmed.html.twig', [
'title' => 'action.confirm.title.resume',
'invoice' => $invoice,
'result_title' => 'action.result.resume.title',
'result_text' => 'action.result.resume.text'
]);
}
return $this->render('app/action/confirm.html.twig', [
'title' => 'action.confirm.title.resume',
'invoice' => $invoice,
'submit_trans' => 'action.confirm.submit.resume',
'want_confirm_level1_trans' => 'action.resume.want.confirm.level1',
'want_confirm_level2_trans' => 'action.resume.want.confirm.level2',
'form' => $form->createView()
]);
}
/**
* @param Request $request
* @param Procedure $procedure
* @param EntityManagerInterface $entityManager
*
* @return Response
* @Route("/action/procedures/{forceDispatchToken}/dispatch", name="procedure_force_dispatch")
*/
public function forceDispatchProcedure(Request $request, Procedure $procedure, EntityManagerInterface $entityManager): Response
{
if (!$procedure->isPending()) {
throw new ConflictHttpException();
}
if (null === $pendingStep = $procedure->getPendingStep()) {
throw new NotFoundHttpException();
}
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getAccountLocale()
);
$form = $this->createForm(ConfirmActionType::class, null, [
'action_label' => 'action.confirm.checkbox.procedure.dispatch'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$procedure->refreshForceDispatchToken();
$pendingStep->setScheduledAt(new DateTime());
$entityManager->flush();
$this->dispatchMessage(new ProceduresDispatch([$procedure->getId()]));
return $this->render('app/action/confirmed.html.twig', [
'title' => 'action.confirm.title.dispatch',
'invoice' => $invoice,
'result_title' => 'action.result.dispatch.title',
'result_text' => 'action.result.dispatch.text'
]);
}
return $this->render('app/action/confirm.html.twig', [
'title' => 'action.confirm.title.dispatch',
'invoice' => $invoice,
'submit_trans' => 'action.confirm.submit.dispatch',
'want_confirm_level1_trans' => 'action.dispatch.want.confirm.level1',
'want_confirm_level2_trans' => 'action.dispatch.want.confirm.level2',
'form' => $form->createView()
]);
}
/**
* @param Request $request
* @param ProcedureStep $procedureStep
* @param string $notificationMethod
* @param EntityManagerInterface $entityManager
*
* @return Response
* @Route("/action/procedure_steps/{notifyToken}/{notificationMethod}", name="procedure_step_notify", requirements={
* "notificationMethod"="mail|registered_post|lawyer"
* })
* @ParamConverter("procedureStep", options={"exclude": {"notificationMethod"}})
*/
public function notifyVia(Request $request, ProcedureStep $procedureStep, string $notificationMethod, EntityManagerInterface $entityManager): Response
{
$procedure = $procedureStep->getProcedure();
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getAccountLocale()
);
// Firstly, check if the procedureStep is related to a step that
// has the property manualNotificationMethod with "TRUE" value.
// If not, throw conflict exception.
$step = $procedureStep->getStep();
if (!$step->isManualNotificationMethod()) {
throw new ConflictHttpException('The given step is not configured for manual notification.');
}
if (!$procedure->isPending()) {
throw new ConflictHttpException('The given procedure is not running.');
}
if (null === $nextProcedureStep = $procedure->getPendingStep()) {
throw new ConflictHttpException('Cannot choose a notification method for a NULL next step.');
}
$form = $this->createForm(ConfirmActionType::class, null, [
'action_label' => "action.confirm.checkbox.procedure.notify_$notificationMethod"
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$nextProcedureStep->setNotificationMethod($notificationMethod);
// Update the notifyToken to disallow multiple calls
$procedureStep->hashNotifyToken();
$entityManager->flush();
$this->eventDispatcher->dispatch(
new ProcedureStepNotificationMethodEvent($procedureStep, $notificationMethod)
);
return $this->render('app/action/confirmed.html.twig', [
'title' => "action.confirm.title.notify_$notificationMethod",
'invoice' => $invoice,
'result_title' => "action.result.notify_$notificationMethod.title",
'result_text' => "action.result.notify_$notificationMethod.text"
]);
}
return $this->render('app/action/confirm.html.twig', [
'title' => "action.confirm.title.notify_$notificationMethod",
'invoice' => $invoice,
'submit_trans' => "action.confirm.submit.notify_$notificationMethod",
'want_confirm_level1_trans' => "action.notify_$notificationMethod.want.confirm.level1",
'want_confirm_level2_trans' => "action.notify_$notificationMethod.want.confirm.level2",
'form' => $form->createView()
]);
}
/**
* @param Request $request
* @param Procedure $procedure
* @param ProcedureClaimFactory $claimFactory
* @param ProcedureClaimRepository $claimRepository
*
* @return Response
* @Route("/action/procedures/{claimToken}/claim", name="procedure_claim")
*/
public function claimProcedure(
Request $request,
Procedure $procedure,
ProcedureClaimFactory $claimFactory,
ProcedureClaimRepository $claimRepository): Response
{
if (!$procedure->isPending()) {
throw new NotFoundHttpException();
}
$invoice = $procedure->getInvoice();
$this->updateLocale(
$request,
$invoice->getCustomerLocale()
);
$form = $this->createForm(ProcedureClaimType::class, $claim = $claimFactory->create($procedure));
$form->handleRequest($request);
$event = new ProcedureClaimEvent($claim);
if ($form->isSubmitted() && $form->isValid()) {
$claimRepository->add($claim);
$this->eventDispatcher->dispatch($event, Events::PROCEDURE_CLAIM_AFTER_CREATE);
return $this->render('app/claim/success.html.twig', [
'procedure' => $procedure,
'invoice' => $invoice
]);
}
return $this->render('app/claim/form.html.twig', [
'form' => $form->createView(),
'invoice' => $invoice
]);
}
/**
* @param Procedure $procedure
* @param Request $request
* @param EntityManagerInterface $entityManager
* @param Client $stripeAccount
* @param GoCardlessAccountManager $goCardlessAccountManager
*
* @return Response
* @Route("/action/procedures/{payToken}/pay", name="procedure_pay")
*/
public function payProcedure(Procedure $procedure, Request $request, EntityManagerInterface $entityManager, Client $stripeAccount, GoCardlessAccountManager $goCardlessAccountManager): Response
{
$invoice = $procedure->getInvoice();
if ($invoice->isPaid()) {
throw new NotFoundHttpException();
}
$this->updateLocale($request, $invoice->getCustomerLocale());
$form = $this->createForm(PayProcedureType::class, new PayProcedureDto(), [
'invoice' => $invoice
]);
$form->handleRequest($request);
$event = new ProcedureInvoicePaidEvent($procedure);
if ($form->isSubmitted() && $form->isValid()) {
// Send notification email
$this->eventDispatcher->dispatch($event, Events::PROCEDURE_INVOICE_PAID);
// Refresh payToken to disallow customer to use the same URL
$procedure->refreshPayToken();
$entityManager->flush();
$this->eventDispatcher->dispatch(
new InvoicePaymentEvent($procedure, InvoicePaymentEvent::PAYMENT_METHOD_TRANSFER)
);
return $this->render('app/pay/paid.html.twig', [
'invoice' => $invoice,
'has_stripe_account' => $stripeAccount->hasAssociatedAccount($invoice)
]);
}
return $this->render('app/pay/pay.html.twig', [
'invoice' => $invoice,
'form' => $form->createView(),
'has_stripe_account' => $stripeAccount->hasAssociatedAccount($invoice),
'has_gocardless_account' => $goCardlessAccountManager->hasAssociatedAccount($invoice)
]);
}
/**
* @param Request $request
* @param Invoice $invoice
* @param GoCardlessMandateRepository $mandateRepository
* @param GoCardlessAccountRepository $accountRepository
* @param GoCardlessService $goCardlessService
*
* @return Response
* @Route("/action/gocardless/{invoice}/mandate", name="gocardless_mandate")
*/
public function gocardlessMandate(
Request $request,
Invoice $invoice,
GoCardlessMandateRepository $mandateRepository,
GoCardlessAccountRepository $accountRepository,
GoCardlessService $goCardlessService): Response
{
if (!$invoice->getProcedure()->isPending()) {
throw new NotFoundHttpException();
}
// Fetch the gocardless account related to user's invoice
if (null === $account = $accountRepository->findOneByAccount($invoice->getAccount())) {
throw new NotFoundHttpException();
}
// The invoice should not have an already submitted mandate otherwise
// Take the existed one and reset it.
if (null === $mandate = $mandateRepository->findOneByInvoice($invoice)) {
$mandate = (new GoCardlessMandate())
->setInvoice($invoice);
} else {
$mandate->reset();
}
$mandate->setChargeDate(
$request->query->getBoolean('at_due') ? $invoice->getDueDate() : new DateTime()
);
// Connect using gocardless account and create a new redirectFlow object
$redirectFlow = $goCardlessService->connect($account->getCredential(GoCardlessAccount::CREDENTIAL_ACCESS_TOKEN))
->redirectFlows()
->create([
'params' => [
'description' => $invoice->getName(),
'session_token' => $request->getSession()->getId(),
'success_redirect_url' => $this->generateUrl('app_page_gocardless_mandate_callback', ['invoice' => $invoice->getId()], UrlGeneratorInterface::ABSOLUTE_URL)
]
]);
// We need to save that, we need it later in the callback
$mandate->setRedirectFlowId($redirectFlow->id);
$mandateRepository->save($mandate);
return $this->redirect($redirectFlow->redirect_url);
}
/**
* @param Request $request
* @param Invoice $invoice
* @param GoCardlessMandateRepository $mandateRepository
* @param GoCardlessAccountRepository $accountRepository
* @param GoCardlessService $goCardlessService
* @param EntityManagerInterface $entityManager
*
* @return Response
* @Route("/action/gocardless/{invoice}/mandate/callback", name="gocardless_mandate_callback")
*/
public function gocardlessMandateCallback(
Request $request,
Invoice $invoice,
GoCardlessMandateRepository $mandateRepository,
GoCardlessAccountRepository $accountRepository,
GoCardlessService $goCardlessService,
EntityManagerInterface $entityManager): Response
{
if (!$invoice->getProcedure()->isPending()) {
throw new NotFoundHttpException();
}
if (null === $mandate = $mandateRepository->findOneByInvoice($invoice)) {
throw new NotFoundHttpException();
}
// Fetch the gocardless account related to user's invoice
if (null === $account = $accountRepository->findOneByAccount($invoice->getAccount())) {
throw new NotFoundHttpException();
}
$entityManager->beginTransaction();
try {
$redirectFlow = $goCardlessService->connect($account->getCredential(GoCardlessAccount::CREDENTIAL_ACCESS_TOKEN))
->redirectFlows()
->complete($mandate->getRedirectFlowId(), [
'params' => ['session_token' => $request->getSession()->getId()]
]);
$mandate
->setMandateId($redirectFlow->links->mandate)
->setCustomerId($redirectFlow->links->customer);
$mandateRepository->save($mandate);
// Pause the procedure related to the invoice.
$this->eventDispatcher->dispatch(new GoCardlessMandateEvent($mandate));
$entityManager->getConnection()->commit();
return $this->render('app/pay/gocardless_mandate_success.html.twig', [
'invoice' => $invoice
]);
} catch (Exception $exception) {
$entityManager->getConnection()->rollBack();
throw new ConflictHttpException('Error while trying to process callback', $exception);
}
}
/**
* @param Invoice $invoice
* @param Client $stripeClient
*
* @return Response
* @Route("/action/invoice/{invoice}/paid", name="invoice_paid")
*/
public function paidStripe(Invoice $invoice, Client $stripeClient): Response
{
return $this->render('app/pay/paid.html.twig', [
'credit_card' => true,
'invoice' => $invoice,
'has_stripe_account' => $stripeClient->hasAssociatedAccount($invoice)
]);
}
/**
* @param Procedure $procedure
* @param Request $request
* @param InvoiceRepository $invoiceRepository
*
* @return Response
* @Route("/action/procedures/{customerContactToken}/contacts", name="procedure_customer_contacts")
*/
public function addCustomerContactProcedure(Procedure $procedure, Request $request, InvoiceRepository $invoiceRepository): Response
{
$invoice = $procedure->getInvoice();
$this->updateLocale($request, $invoice->getCustomerLocale());
$form = $this->createForm(InvoiceCustomerContactType::class, $invoice);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$invoiceRepository->save($invoice);
return $this->render('app/customer_contact/add_contact_success.html.twig', [
'invoice' => $invoice
]);
}
return $this->render('app/customer_contact/add_contact.html.twig', [
'invoice' => $invoice,
'form' => $form->createView()
]);
}
/**
* @param Request $request
* @param Procedure $procedure
*
* @return Response
* @Route("/action/procedures/{procedure}/documents", name="procedure_documents")
*/
public function documents(Request $request, Procedure $procedure): Response
{
$invoice = $procedure->getInvoice();
$this->updateLocale($request, $invoice->getCustomerLocale());
return $this->render('app/action/documents.html.twig', [
'title' => 'action.documents.title',
'invoice' => $invoice
]);
}
/**
* @param Procedure $procedure
* @param UploaderHelper $uploaderHelper
*
* @return Response
* @Route("/action/procedures/{procedure}/invoice", name="procedure_invoice")
*/
public function invoice(Procedure $procedure, UploaderHelper $uploaderHelper): Response
{
if (null === $invoice = $procedure->getInvoice()) {
throw new NotFoundHttpException();
}
if (null !== $url = $invoice->getFullFileUrl()) {
return $this->redirect($url);
}
return $this->redirect($uploaderHelper->asset($invoice->getInvoiceFile()));
}
/**
* @return Response
* @Route("/quickbooksintegrationdisconnected")
*/
public function quickbooksIntegrationDisconnected(): Response
{
return $this->render('app/quickbooks_integration_disconnected.html.twig');
}
protected function updateLocale(Request $request, ?string $locale): void
{
$locale = $locale ?? 'fr';
$request->getSession()->set('_locale', $locale);
$this->translator->setLocale($locale);
}
/**
* @param InvoiceRepository $invoiceRepository
* @param Driver $driver
* @param ConnectorRepository $connectorRepository
*
* @return Response
* @Route("/drivers")
*/
public function drivers(InvoiceRepository $invoiceRepository, Driver $driver, ConnectorRepository $connectorRepository): Response
{
$driver->connect(
$connectorRepository->find(1)
)->createPayment(
$invoiceRepository->find(314)
);
return $this->json(
['done']
);
}
}