<?php
namespace App\Service\App\Merchant\Promotion\Listener;
use App\Core\Exception\UnexpectedException;
use App\DTO\Common\Subscription\Response\PlanResponseDTO;
use App\DTO\MerchantApi\Common\Request\LineDiscountRequestDTO;
use App\Entity\Merchant\PromotionEntity;
use App\Repository\Merchant\Contract\IOrderRepository;
use App\Repository\Merchant\Contract\IPlanPriceRepository;
use App\Repository\Merchant\Contract\IPlanRepository;
use App\Repository\Merchant\Contract\IPlanSubscriptionRepository;
use App\Repository\Merchant\Contract\IPromotionRepository;
use App\Repository\Merchant\Contract\IRecurringProfileRepository;
use App\Service\App\Merchant\CustomFeature\Implementation\PromotionCustomFeature;
use App\Service\App\Merchant\Subscription\Event\BuildSubscriptionPaymentLineEvent;
use App\Service\App\Merchant\Subscription\Event\CalculateSubscriptionTotalsEvent;
use App\Service\App\Merchant\Subscription\Event\CreateSubscriptionWithPaymentEvent;
use App\Service\App\Merchant\Subscription\Event\SubscriptionPlanListEvent;
use App\Types\DiscountType;
use App\Types\PromotionType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SubscriptionListener implements EventSubscriberInterface
{
/**
* Plan subscription repository
*
* @var IPlanSubscriptionRepository
*/
private IPlanSubscriptionRepository $planSubscriptionRepository;
/**
* Promotion custom feature
*
* @var PromotionCustomFeature
*/
private PromotionCustomFeature $promotionCustomFeature;
/**
* Order repository
*
* @var IOrderRepository
*/
private IOrderRepository $orderRepository;
/**
* Recurring profile repository
*
* @var IRecurringProfileRepository
*/
private IRecurringProfileRepository $recurringProfileRepository;
/**
* Promotion repository
*
* @var IPromotionRepository
*/
private IPromotionRepository $promotionRepository;
/**
* Plan price repository
*
* @var IPlanPriceRepository
*/
private IPlanPriceRepository $planPriceRepository;
/**
* Plan repository
*
* @var IPlanRepository
*/
private IPlanRepository $planRepository;
/**
* Promotion entity
*
* @var PromotionEntity|null
*/
private ?PromotionEntity $promotion = null;
/**
* Constructor
*
* @param IPlanSubscriptionRepository $planSubscriptionRepository
* @param PromotionCustomFeature $promotionCustomFeature
* @param IOrderRepository $orderRepository
* @param IRecurringProfileRepository $recurringProfileRepository
* @param IPromotionRepository $promotionRepository
* @param IPlanPriceRepository $planPriceRepository
* @param IPlanRepository $planRepository
*/
public function __construct(
IPlanSubscriptionRepository $planSubscriptionRepository,
PromotionCustomFeature $promotionCustomFeature,
IOrderRepository $orderRepository,
IRecurringProfileRepository $recurringProfileRepository,
IPromotionRepository $promotionRepository,
IPlanPriceRepository $planPriceRepository,
IPlanRepository $planRepository
)
{
$this->planSubscriptionRepository = $planSubscriptionRepository;
$this->promotionCustomFeature = $promotionCustomFeature;
$this->orderRepository = $orderRepository;
$this->recurringProfileRepository = $recurringProfileRepository;
$this->promotionRepository = $promotionRepository;
$this->planPriceRepository = $planPriceRepository;
$this->planRepository = $planRepository;
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and respective
* priorities, or 0 if unset
*
* For instance:
*
* * ['eventName' => 'methodName']
* * ['eventName' => ['methodName', $priority]]
* * ['eventName' => [['methodName1', $priority], ['methodName2']]]
*
* The code must not depend on runtime state as it will only be called at compile time.
* All logic depending on runtime state must be put into the individual methods handling the events.
*
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
*/
public static function getSubscribedEvents(): array
{
return [
BuildSubscriptionPaymentLineEvent::NAME => 'onBuildSubscriptionPaymentLine',
CreateSubscriptionWithPaymentEvent::NAME => 'onCreateSubscriptionPayment',
CalculateSubscriptionTotalsEvent::NAME => 'onCalculateSubscriptionTotals',
SubscriptionPlanListEvent::NAME => 'onSubscriptionPlanList'
];
}
/**
* On build subscription payment line
*
* @param BuildSubscriptionPaymentLineEvent $event
* @throws UnexpectedException
*/
public function onBuildSubscriptionPaymentLine(BuildSubscriptionPaymentLineEvent $event): void
{
if (!$this->promotionCustomFeature->isEnabled()) {
return;
}
/** @var PromotionEntity|null $promotion */
$promotion = null;
// If update subscription we use the same promo code and do not allow to change it
// To not allow users cancel in the middle of the subscription and then use this promo code for other payment
if ($event->getRequest()->getSubscriptionId()) {
// Try to get discount from parent subscription order
$subscription = $this->planSubscriptionRepository->getSubscription(
$event->getRequest()->getSubscriptionId()
);
$promotion = $subscription->getRelatedPayment()?->getPromotion();
}
// #30258
// Find promotion by code
//if (!$event->getRequest()->getSubscriptionId() && !is_null($event->getRequest()->getPromoCode())) {
if (!is_null($event->getRequest()->getPromoCode())) {
// Try to find discount
$promotion = $this->promotionRepository->getPromotionByCode(
$event->getRequest()->getPromoCode()
);
}
if ($promotion) {
if ($this->promotion && $this->promotion->getId() != $promotion->getId()) {
throw new UnexpectedException('Multiple promotions are not allowed in one subscription.');
}
}
// Get plan price
$planPrice = $this->planPriceRepository->getPrice(
$event->getSubscriptionItem()->getPriceId()
);
// Apply discount
if ($promotion?->canBeAppliedToPlan($planPrice?->getPlan(), $event->isRecurring())) {
$discountValue = min($promotion->getDiscount(), 1);
$event->getPaymentLine()->addDiscount(
LineDiscountRequestDTO::build(
$discountValue, DiscountType::DISCOUNT_TYPE_PERCENTAGE
)
);
$this->promotion = $promotion;
}
}
/**
* On create subscription payment
*
* @param CreateSubscriptionWithPaymentEvent $event
* @return void
*/
public function onCreateSubscriptionPayment(CreateSubscriptionWithPaymentEvent $event): void
{
if (!$this->promotionCustomFeature->isEnabled()) {
return;
}
// if promotion was used when lines were built
if ($this->promotion) {
// get order and add promotion
if ($event->getOrderId()) {
$order = $this->orderRepository->getOrder($event->getOrderId());
if ($order) {
$this->promotion->addOrder($order);
}
}
// if applicable for recurring profile, add promotion to recurring profile
if ($this->promotion->getType() != PromotionType::TYPE_ONE_TIME_PAYMENT && $event->getRecurringProfileId()) {
$profile = $this->recurringProfileRepository->getRecurringProfile($event->getRecurringProfileId());
if ($profile) {
$this->promotion->addRecurringProfile($profile);
}
}
// save promotion links
$this->promotionRepository->save($this->promotion);
}
// reset promotion
$this->promotion = null;
}
/**
* On calculate subscription totals
*
* @param CalculateSubscriptionTotalsEvent $event
* @return void
*/
public function onCalculateSubscriptionTotals(CalculateSubscriptionTotalsEvent $event): void
{
if (!$this->promotionCustomFeature->isEnabled()) {
return;
}
// if it is update subscription, we do not allow to use promo code.
if ($event->getRequest()->getSubscriptionId()) {
// #30258
// return;
}
if (count($event->getResponse()->getLines())) {
$allFree = true;
foreach ($event->getResponse()->getLines() as $line) {
$allFree &= $line->getPrice() == 0;
}
if ($allFree) {
return;
}
}
// check if promotion is available
$event->getResponse()->setPromotionAvailable(
$this->promotionRepository->hasActivePromotions(
$event->getRequest()->getCustomerReferenceId()
)
);
}
/**
* On subscription plan list
*
* @param SubscriptionPlanListEvent $event
* @return void
*/
public function onSubscriptionPlanList(SubscriptionPlanListEvent $event): void
{
if (!$this->promotionCustomFeature->isEnabled()) {
return;
}
// if promo code is set, apply discount to plans
if ($event->getRequest()->getPromoCode()) {
// get promotion by code
$promotion = $this->promotionRepository->getPromotionByCode(
$event->getRequest()->getPromoCode()
);
// if not promotion or not active, return
if (!$promotion || !$promotion->isActive()) {
return;
}
if ($event->getRequest()->getCustomerReferenceId()) {
// check if customer can use promotion
if (!$this->promotionRepository->canCustomerUsePromotion(
$event->getRequest()->getCustomerReferenceId(),
$event->getRequest()->getPromoCode()
)) {
return;
}
}
// apply discount to plans
foreach ($event->getPlans() as $plan) {
// get plan entity
$planEntity = $this->planRepository->getPlan($plan->getId());
// if applicable, apply discount
if ($promotion->canBeAppliedToPlan($planEntity)) {
$plan->setDiscount($promotion->getDiscount());
}
// only plans have additions
if (!($plan instanceof PlanResponseDTO)) {
continue;
}
// apply discount to plan additions
foreach ($plan->getAdditions() as $addition) {
// get addition entity
$additionEntity = $this->planRepository->getPlan($addition->getId());
// if applicable, apply discount
if ($promotion->canBeAppliedToPlan($additionEntity)) {
$addition->setDiscount($promotion->getDiscount());
}
}
}
}
}
}