Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/doctrine/Invoice.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<field name="localeCode" column="locale_code" />
<field name="total" column="total" type="integer" />
<field name="paymentState" column="payment_state" />
<field name="pdfSent" column="pdf_sent" type="boolean" />

<one-to-one field="billingData" target-entity="Sylius\InvoicingPlugin\Entity\BillingDataInterface">
<cascade>
Expand Down
12 changes: 12 additions & 0 deletions config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,25 @@
<parameters>
<parameter key="sylius_invoicing.default_logo_file">@SyliusInvoicingPlugin/assets/sylius-logo.png</parameter>
<parameter key="sylius_invoicing.template.logo_file">%env(default:sylius_invoicing.default_logo_file:resolve:SYLIUS_INVOICING_LOGO_FILE)%</parameter>
<parameter key="sylius_invoicing.email.retry_max_attempts">3</parameter>
<parameter key="sylius_invoicing.email.retry_delay_ms">60000</parameter>
</parameters>

<services>
<service id="sylius_invoicing.email.invoice_email_retry_scheduler" class="Sylius\InvoicingPlugin\Email\InvoiceEmailRetryScheduler">
<argument type="service" id="sylius.command_bus" />
<argument>%sylius_invoicing.email.retry_max_attempts%</argument>
<argument>%sylius_invoicing.email.retry_delay_ms%</argument>
<argument type="service" id="logger" on-invalid="null" />
</service>
<service id="Sylius\InvoicingPlugin\Email\InvoiceEmailRetrySchedulerInterface" alias="sylius_invoicing.email.invoice_email_retry_scheduler" />

<service id="sylius_invoicing.email.invoice_email_sender" class="Sylius\InvoicingPlugin\Email\InvoiceEmailSender">
<argument type="service" id="sylius.email_sender" />
<argument type="service" id="sylius_invoicing.provider.invoice_file" />
<argument>%sylius_invoicing.pdf_generator.enabled%</argument>
<argument type="service" id="logger" on-invalid="null" />
<argument type="service" id="sylius_invoicing.email.invoice_email_retry_scheduler" on-invalid="null" />
</service>
<service id="Sylius\InvoicingPlugin\Email\InvoiceEmailSenderInterface" alias="sylius_invoicing.email.invoice_email_sender" />

Expand Down
6 changes: 6 additions & 0 deletions config/services/cli.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@
<argument type="service" id="sylius.repository.order" />
<tag name="console.command" />
</service>
<service id="sylius_invoicing.cli.retry_failed_invoices" class="Sylius\InvoicingPlugin\Cli\RetryFailedInvoicesCommand">
<argument type="service" id="sylius_invoicing.repository.invoice" />
<argument type="service" id="sylius.command_bus" />
<argument type="service" id="logger" />
<tag name="console.command" />
</service>
</services>
</container>
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ parameters:
- '/Method Sylius\\InvoicingPlugin\\Security\\Voter\\InvoiceVoter::supports\(\) has parameter \$attribute with no typehint specified./'
- '/Method Sylius\\InvoicingPlugin\\Security\\Voter\\InvoiceVoter::supports\(\) has parameter \$subject with no typehint specified./'
- '/expects string, string\|null given\.$/'
- '/Method Sylius\\Component\\Mailer\\Sender\\SenderInterface::send\(\) invoked with 7 parameters, 2-5 required\./'
78 changes: 78 additions & 0 deletions src/Cli/RetryFailedInvoicesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Cli;

use Psr\Log\LoggerInterface;
use Sylius\InvoicingPlugin\Command\SendInvoiceEmail;
use Sylius\InvoicingPlugin\Doctrine\ORM\InvoiceRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\Exception\ExceptionInterface;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsCommand(
name: 'sylius-invoicing:retry-failed-invoices',
description: 'Retries sending invoice emails for invoices with missing PDF attachments.',
)]
final class RetryFailedInvoicesCommand extends Command
{
public function __construct(
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly MessageBusInterface $commandBus,
private readonly LoggerInterface $logger,
) {
parent::__construct();
}

public function execute(InputInterface $input, OutputInterface $output): int
{
$failedInvoices = $this->invoiceRepository->findUnsent();

if ([] === $failedInvoices) {
$output->writeln('No failed invoices found to retry.');

return Command::SUCCESS;
}

$dispatched = 0;

foreach ($failedInvoices as $invoice) {
$orderNumber = $invoice->order()->getNumber();

if (null === $orderNumber) {
continue;
}

try {
$this->commandBus->dispatch(new SendInvoiceEmail($orderNumber));
} catch (ExceptionInterface $e) {
$this->logger->error(
sprintf(
'Failed to dispatch invoice resend command for order %s: %s',
$orderNumber,
$e->getMessage(),
),
['exception' => $e],
);
}
++$dispatched;
}

$output->writeln(sprintf('Dispatched %d invoice resend command(s).', $dispatched));

return Command::SUCCESS;
}
}
13 changes: 10 additions & 3 deletions src/Command/SendInvoiceEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@

namespace Sylius\InvoicingPlugin\Command;

final class SendInvoiceEmail
final readonly class SendInvoiceEmail
{
public function __construct(private readonly string $orderNumber)
{
public function __construct(
private string $orderNumber,
private int $attempt = 0,
) {
}

public function orderNumber(): string
{
return $this->orderNumber;
}

public function attempt(): int
{
return $this->attempt;
}
}
19 changes: 14 additions & 5 deletions src/CommandHandler/SendInvoiceEmailHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
use Sylius\InvoicingPlugin\Email\InvoiceEmailSenderInterface;
use Sylius\InvoicingPlugin\Entity\InvoiceInterface;

final class SendInvoiceEmailHandler
final readonly class SendInvoiceEmailHandler
{
public function __construct(
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly OrderRepositoryInterface $orderRepository,
private readonly InvoiceEmailSenderInterface $emailSender,
private InvoiceRepositoryInterface $invoiceRepository,
private OrderRepositoryInterface $orderRepository,
private InvoiceEmailSenderInterface $emailSender,
) {
}

Expand All @@ -48,6 +48,15 @@ public function __invoke(SendInvoiceEmail $command): void
return;
}

$this->emailSender->sendInvoiceEmail($invoice, $customer->getEmail());
$customerEmail = $customer->getEmail();
if (null === $customerEmail) {
return;
}

$this->emailSender->sendInvoiceEmail($invoice, $customerEmail, $command->attempt());

if ($invoice->isPdfSent()) {
$this->invoiceRepository->add($invoice);
}
}
}
15 changes: 15 additions & 0 deletions src/Doctrine/ORM/InvoiceRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,19 @@ public function findByOrderNumber(string $orderNumber): array

return $invoices;
}

public function findUnsent(): array
{
$invoices = $this
->createQueryBuilder('invoice')
->where('invoice.pdfSent = :pdfSent')
->setParameter('pdfSent', false)
->getQuery()
->getResult()
;

Assert::isArray($invoices);

return $invoices;
}
}
5 changes: 5 additions & 0 deletions src/Doctrine/ORM/InvoiceRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ interface InvoiceRepositoryInterface extends RepositoryInterface
public function findOneByOrder(OrderInterface $order): ?InvoiceInterface;

public function findByOrderNumber(string $orderNumber): array;

/**
* @return array<InvoiceInterface>
*/
public function findUnsent(): array;
}
84 changes: 84 additions & 0 deletions src/Email/InvoiceEmailRetryScheduler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Email;

use Psr\Log\LoggerInterface;
use Sylius\InvoicingPlugin\Command\SendInvoiceEmail;
use Sylius\InvoicingPlugin\Entity\InvoiceInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\ExceptionInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;

final class InvoiceEmailRetryScheduler implements InvoiceEmailRetrySchedulerInterface
{
public function __construct(
private readonly MessageBusInterface $commandBus,
private readonly int $maxAttempts,
private readonly int $retryDelayMilliseconds,
private readonly ?LoggerInterface $logger = null,
) {
}

public function scheduleRetry(InvoiceInterface $invoice, string $customerEmail, int $attempt): void
{
if ($attempt >= $this->maxAttempts) {
$this->logger?->warning(
sprintf(
'Invoice email retry aborted for invoice "%s" (%s) after %d attempts.',
$invoice->number(),
$invoice->id(),
$attempt,
),
['customerEmail' => $customerEmail],
);

return;
}

$orderNumber = $invoice->order()->getNumber();
if (null === $orderNumber) {
$this->logger?->warning(
sprintf(
'Invoice email retry skipped because order number is missing for invoice "%s" (%s).',
$invoice->number(),
$invoice->id(),
),
['customerEmail' => $customerEmail],
);

return;
}

$nextAttempt = $attempt + 1;
$message = new SendInvoiceEmail($orderNumber, $nextAttempt);
$envelope = new Envelope($message, [new DelayStamp($this->retryDelayMilliseconds)]);

try {
$this->commandBus->dispatch($envelope);
} catch (ExceptionInterface $e) {
$this->logger?->error($e->getMessage());
}

$this->logger?->info(
sprintf(
'Scheduled invoice email retry #%d for invoice "%s" (%s) to "%s".',
$nextAttempt,
$invoice->number(),
$invoice->id(),
$customerEmail,
),
);
}
}
21 changes: 21 additions & 0 deletions src/Email/InvoiceEmailRetrySchedulerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Email;

use Sylius\InvoicingPlugin\Entity\InvoiceInterface;

interface InvoiceEmailRetrySchedulerInterface
{
public function scheduleRetry(InvoiceInterface $invoice, string $customerEmail, int $attempt): void;
}
Loading