Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
drudge committed Dec 7, 2024
0 parents commit 2c44486
Show file tree
Hide file tree
Showing 18 changed files with 1,042 additions and 0 deletions.
55 changes: 55 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Cache and logs (Symfony2)
/app/cache/*
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep

# Email spool folder
/app/spool/*

# Cache, session files and logs (Symfony3)
/var/cache/*
/var/logs/*
/var/sessions/*
!var/cache/.gitkeep
!var/logs/.gitkeep
!var/sessions/.gitkeep

# Logs (Symfony4)
/var/log/*
!var/log/.gitkeep

# Parameters
/app/config/parameters.yml
/app/config/parameters.ini

# Managed by Composer
/app/bootstrap.php.cache
/var/bootstrap.php.cache
/bin/*
!bin/console
!bin/symfony_requirements
/vendor/

# Assets and user uploads
/web/bundles/
/web/uploads/

# PHPUnit
/app/phpunit.xml
/phpunit.xml

# Build data
/build/

# Composer PHAR
/composer.phar
.php-cs-fixer.cache
composer.lock

# Backup entities generated with doctrine:generate:entities command
**/Entity/*~

# Embedded web-server pid file
/.web-server-pid
.DS_Store
84 changes: 84 additions & 0 deletions Controller/InvoiceEmailerController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace KimaiPlugin\InvoiceEmailerBundle\Controller;

use App\Configuration\LocaleService;
use App\Configuration\MailConfiguration;
use App\Configuration\SystemConfiguration;
use App\Entity\Invoice;
use App\Invoice\ServiceInvoice;
use App\Repository\InvoiceRepository;
use App\Utils\FileHelper;
use Doctrine\ORM\EntityManagerInterface;
use KimaiPlugin\InvoiceEmailerBundle\EventSubscriber\EmailSentFieldSubscriber;
use KimaiPlugin\InvoiceEmailerBundle\Service\InvoiceEmailService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;

#[Route(path: '/invoice')]
#[IsGranted('email_invoice')]
final class InvoiceEmailerController extends AbstractController
{
public function __construct(
private TranslatorInterface $translator,
private MailerInterface $mailer,
private ServiceInvoice $serviceInvoice,
private InvoiceRepository $invoiceRepository,
private FileHelper $fileHelper,
private SystemConfiguration $configuration,
private MailConfiguration $mailConfiguration,
private ParameterBagInterface $params,
private EntityManagerInterface $entityManager,
private EventDispatcherInterface $eventDispatcher,
private EmailSentFieldSubscriber $emailSentFieldSubscriber,
private LocaleService $localeService,
private InvoiceEmailService $invoiceEmailService,
private readonly LoggerInterface $logger
) {
}

#[Route(path: '/emailer/send/{id}', name: 'invoice_emailer_send', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function emailInvoice(Invoice $invoice): RedirectResponse
{
try {
$customer = $invoice->getCustomer();
$email = $customer->getEmail();
$user = $this->getUser();

if (empty($email)) {
throw new \Exception('Customer has no email address');
}

$invoiceFile = $this->serviceInvoice->getInvoiceFile($invoice);
if ($invoiceFile === null) {
throw new \Exception('Invoice file not found');
}

$this->logger->debug('Manual invoice email requested', [
'invoice_id' => $invoice->getId(),
'user_id' => $user ? $user->getId() : null,
]);

$success = $this->invoiceEmailService->send($invoice, true, $user);

if ($success) {
$this->addFlash('success', $this->translator->trans('invoice.emailer_sent', [], 'system-configuration'));
} else {
$this->addFlash('error', $this->translator->trans('invoice.emailer_error', [], 'system-configuration'));
}
} catch (\Exception $e) {
$this->addFlash('error', $e->getMessage());
}

return $this->redirectToRoute('admin_invoice_list');
}
}
43 changes: 43 additions & 0 deletions DependencyInjection/InvoiceEmailerExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace KimaiPlugin\InvoiceEmailerBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class InvoiceEmailerExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yaml');
}

public function prepend(ContainerBuilder $container)
{
$container->prependExtensionConfig('kimai', [
'permissions' => [
'roles' => [
'ROLE_SUPER_ADMIN' => [
'email_invoice',
],
'ROLE_ADMIN' => [
'email_invoice',
],
'ROLE_EMAIL_INVOICE' => [
'email_invoice',
],
],
],
]);

$container->prependExtensionConfig('security', [
'role_hierarchy' => [
'ROLE_SUPER_ADMIN' => ['ROLE_EMAIL_INVOICE']
]
]);
}
}
58 changes: 58 additions & 0 deletions EventSubscriber/EmailSentFieldSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace KimaiPlugin\InvoiceEmailerBundle\EventSubscriber;

use App\Configuration\SystemConfiguration;
use App\Entity\EntityWithMetaFields;
use App\Entity\InvoiceMeta;
use App\Entity\MetaTableTypeInterface;
use App\Event\InvoiceMetaDefinitionEvent;
use App\Event\InvoiceMetaDisplayEvent;
use App\Form\Type\DateTimePickerType;
use App\Form\Type\DateTimeTextType;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use KimaiPlugin\InvoiceEmailerBundle\Form\Type\MetaDateTimePickerType;

class EmailSentFieldSubscriber implements EventSubscriberInterface
{
public function __construct(
private SystemConfiguration $configuration,
private LoggerInterface $logger
) {
}

public static function getSubscribedEvents(): array
{
return [
InvoiceMetaDefinitionEvent::class => ['loadEmailSentDateMetaField', 200],
InvoiceMetaDisplayEvent::class => ['addEmailSentField', 75],
];
}

public function prepareField(MetaTableTypeInterface $definition, EntityWithMetaFields $entity = null): MetaTableTypeInterface
{
$timezone = $this->configuration->find('defaults.user.timezone');

$definition->setName('email_sent_date');
$definition->setLabel('Email date');
$definition->setIsVisible(true);
$definition->setType(MetaDateTimePickerType::class);

if ($entity !== null) {
$entity->setMetaField($definition);
}

return $definition;
}

public function loadEmailSentDateMetaField(InvoiceMetaDefinitionEvent $event): void
{
$this->prepareField(new InvoiceMeta(), $event->getEntity());
}

public function addEmailSentField(InvoiceMetaDisplayEvent $event): void
{
$event->addField($this->prepareField(new InvoiceMeta()));
}
}
69 changes: 69 additions & 0 deletions EventSubscriber/InvoiceActionsSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace KimaiPlugin\InvoiceEmailerBundle\EventSubscriber;

use App\Entity\Invoice;
use App\EventSubscriber\Actions\AbstractActionsSubscriber;
use App\Event\InvoiceCreatedEvent;
use App\Event\InvoiceStatusUpdateEvent;
use App\Event\PageActionsEvent;
use KimaiPlugin\InvoiceEmailerBundle\Service\InvoiceEmailService;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class InvoiceActionsSubscriber extends AbstractActionsSubscriber
{
public function __construct(
private readonly AuthorizationCheckerInterface $auth,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly InvoiceEmailService $invoiceEmailer,
private readonly LoggerInterface $logger
) {
parent::__construct($auth, $urlGenerator);
}

public static function getActionName(): string
{
return 'invoice';
}

public function onActions(PageActionsEvent $event): void
{
$payload = $event->getPayload();
$this->logger->debug('InvoiceActionsSubscriber::onActions called', [
'payload_type' => gettype($payload),
'has_invoice_key' => is_array($payload) ? array_key_exists('invoice', $payload) : false,
]);

if (!\is_array($payload) || !\array_key_exists('invoice', $payload)) {
$this->logger->debug('InvoiceActionsSubscriber::onActions returning early - invalid payload');
return;
}

$invoice = $payload['invoice'];
$this->logger->debug('InvoiceActionsSubscriber::onActions invoice check', [
'is_invoice' => $invoice instanceof Invoice,
]);

if (!$invoice instanceof Invoice) {
return;
}

if (!$this->auth->isGranted('email_invoice')) {
return;
}

$meta = $invoice->getMetaField('email_sent_date');
$title = $meta !== null && $meta->getValue() ? 'invoice.emailer_resend' : 'invoice.emailer_send';

if (!$invoice->isCanceled()) {
$event->addDivider();
$event->addAction('email_invoice', [
'url' => $this->path('invoice_emailer_send', ['id' => $invoice->getId()]),
'title' => $title,
'translation_domain' => 'system-configuration',
]);
}
}
}
82 changes: 82 additions & 0 deletions EventSubscriber/InvoiceSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace KimaiPlugin\InvoiceEmailerBundle\EventSubscriber;

use App\Event\InvoiceCreatedEvent;
use App\Event\InvoiceStatusUpdateEvent;
use KimaiPlugin\InvoiceEmailerBundle\Service\InvoiceEmailService;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class InvoiceSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly AuthorizationCheckerInterface $auth,
private readonly InvoiceEmailService $invoiceEmailer,
private readonly LoggerInterface $logger
) {
}

public static function getSubscribedEvents(): array
{
return [
InvoiceCreatedEvent::class => ['onInvoiceCreated', 100],
InvoiceStatusUpdateEvent::class => ['onInvoiceStatusUpdate', 100],
];
}

public function onInvoiceCreated(InvoiceCreatedEvent $event): void
{
if (!$this->auth->isGranted('email_invoice')) {
return;
}

$invoice = $event->getInvoice();
$user = null;

$this->logger->debug('onInvoiceCreated triggered', [
'invoice_id' => $invoice->getId(),
'invoice_status' => $invoice->getStatus(),
'user_id' => $user ? $user->getId() : null,
]);

if ($this->invoiceEmailer->shouldSendEmail($invoice)) {
$this->logger->debug('Attempting to send email for new invoice', [
'invoice_id' => $invoice->getId(),
]);
$result = $this->invoiceEmailer->send($invoice, false, $user);
$this->logger->debug('Email send attempt completed', [
'invoice_id' => $invoice->getId(),
'success' => $result,
]);
}
}

public function onInvoiceStatusUpdate(InvoiceStatusUpdateEvent $event): void
{
if (!$this->authorizationChecker->isGranted('email_invoice')) {
return;
}

$invoice = $event->getInvoice();
$user = null;

$this->logger->debug('onInvoiceStatusUpdate triggered', [
'invoice_id' => $invoice->getId(),
'invoice_status' => $invoice->getStatus(),
'user_id' => $user ? $user->getId() : null,
]);

if ($this->invoiceEmailer->shouldSendEmail($invoice)) {
$this->logger->debug('Attempting to send email for status update', [
'invoice_id' => $invoice->getId(),
]);
$result = $this->invoiceEmailer->send($invoice, false, $user);
$this->logger->debug('Email send attempt completed', [
'invoice_id' => $invoice->getId(),
'success' => $result,
]);
}
}
}
Loading

0 comments on commit 2c44486

Please sign in to comment.