<?php
declare(strict_types=1);
namespace App\License\Service;
use App\License\Enum\LicenseFeature;
use App\License\Enum\LicenseStatus;
use App\License\Repository\LicenseRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class SyliusOrderPaidSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LicenseRepository $licenseRepository,
private readonly LicenseFactory $licenseFactory,
private readonly LicenseAuditLogger $auditLogger,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
'sylius.payment.post_complete' => 'onPaymentCompleted',
];
}
/**
* @param mixed $event
*/
public function onPaymentCompleted($event): void
{
if (!method_exists($event, 'getSubject')) {
return;
}
$payment = $event->getSubject();
if (!method_exists($payment, 'getOrder') || null === $payment->getOrder()) {
return;
}
$order = $payment->getOrder();
if (!$order instanceof OrderInterface) {
return;
}
if (null === $order->getCustomer() || null === $order->getCustomer()->getEmail()) {
return;
}
$userEmail = (string) $order->getCustomer()->getEmail();
$userName = (string) ($order->getCustomer()->getFullName() ?: $userEmail);
$features = $this->resolveFeaturesFromOrder($order);
$months = $this->resolveSubscriptionMonths($order);
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$start = $order->getCheckoutCompletedAt() ? \DateTimeImmutable::createFromInterface($order->getCheckoutCompletedAt()) : $now;
$end = $start->modify(sprintf('+%d months', max(1, $months)));
$license = $this->licenseRepository->findOneBy(['userEmail' => $userEmail]);
$oldValue = null;
$action = 'created_by_order';
if (null === $license) {
$license = $this->licenseFactory->createLicense(
$userName,
$userEmail,
$features,
$start,
$end,
1,
false,
'sylius_order',
null,
LicenseStatus::ACTIVE
);
$this->entityManager->persist($license);
} else {
$oldValue = [
'subscriptionEnd' => $license->getSubscriptionEnd()->format(\DATE_ATOM),
'activeFeatures' => $license->getActiveFeatures(),
'status' => $license->getStatus(),
];
$license->setUserName($userName);
$license->setUserEmail($userEmail);
$license->setActiveFeatures($features);
$license->setSubscriptionStart($start);
$license->setSubscriptionEnd($end);
$license->setStatus(LicenseStatus::ACTIVE);
$license->setOrigin('sylius_order');
$license->setIsFreeGrant(false);
$license->setAmountPaid($this->licenseFactory->calculateAnnualPrice($features));
$license->touch();
$action = 'updated_by_order';
}
$this->entityManager->persist($license);
$this->auditLogger->log($license, null, $action, $oldValue, [
'subscriptionStart' => $license->getSubscriptionStart()->format(\DATE_ATOM),
'subscriptionEnd' => $license->getSubscriptionEnd()->format(\DATE_ATOM),
'activeFeatures' => $license->getActiveFeatures(),
'amountPaid' => $license->getAmountPaid(),
]);
$this->entityManager->flush();
$this->logger->info('License updated from paid Sylius order.', [
'licenseUuid' => $license->getUuid(),
'orderNumber' => method_exists($order, 'getNumber') ? $order->getNumber() : null,
'email' => $userEmail,
]);
}
/**
* @return array<int, string>
*/
private function resolveFeaturesFromOrder(OrderInterface $order): array
{
$features = [];
foreach ($order->getItems() as $item) {
$codes = [];
if (method_exists($item, 'getVariant') && null !== $item->getVariant() && method_exists($item->getVariant(), 'getCode')) {
$codes[] = strtoupper((string) $item->getVariant()->getCode());
}
if (method_exists($item, 'getProduct') && null !== $item->getProduct()) {
$product = $item->getProduct();
if (method_exists($product, 'getCode')) {
$codes[] = strtoupper((string) $product->getCode());
}
if (method_exists($product, 'hasAttributeByCodeAndLocale')) {
foreach (['audio', 'video', 'light', 'control'] as $featureCode) {
if ($product->hasAttributeByCodeAndLocale($featureCode)) {
$features[] = strtoupper($featureCode);
}
}
}
}
foreach ($codes as $code) {
if ('STUDIO' === $code) {
$features = array_merge($features, LicenseFeature::allDefaults());
continue;
}
$parts = explode('_', $code);
foreach ($parts as $part) {
if (in_array($part, LicenseFeature::allDefaults(), true)) {
$features[] = $part;
}
}
}
}
return array_values(array_unique($features));
}
private function resolveSubscriptionMonths(OrderInterface $order): int
{
$total = (int) $order->getTotal();
if ($total <= 0) {
return 12;
}
return max(1, (int) ceil($total / max(1, count($this->resolveFeaturesFromOrder($order)) * LicenseFeature::PRICE_PER_FEATURE)));
}
}