Sylius Promotion Component, o cómo inspirarse en un proyecto Open Source

Hace unas semanas, dentro del contexto del proyecto en el que actualmente trabajo, se propuso diseñar una funcionalidad basada en descuentos y promociones. Nuestra arquitectura basada en microservicios con el stack NestJS y Typescript está basada en la premisa de desacoplar al máximo los conceptos para una mantenibilidad adecuada y un proceso evolutivo que nos permita escalar nuestro diseño de forma ágil y adaptado al contexto de un equipo dividido en dos squads.

Para ello, se analizaron los requisitos a corto y medio plazo que requeriría nuestro proceso de compra y escenarios que “Negocio” podría plantear para aplicar estos descuentos. Aún siendo un MVP, se buscaba plantear un diseño lo más flexible posible que nos permitiera ofrecer al equipo de Marketing la flexibilidad de plantear estrategias de promociones que requirieran el menor esfuerzo de desarrollo posible.

Poniendo en valor las experiencias pasadas

Sacando de la chistera esas experiencias del pasado, aproveché mi experiencia trabajando con soluciones e-commerce y decidí confiar una vez más en Sylius, framework open source para la construcción de e-commerce basado en Symfony del cual ya he hablado anteriormente en este blog.

Sylius se basa en una serie de componentes, todos ellos desacoplados entre sí y que, trabajando en conjunto, ofrecen una solución e-commerce completa. ¿Por qué confiar en un proyecto como este? Pues porque nace de una comunidad con más de 600 contribuidores, una base de 250 plugins y 6000 estrellas en Github, lo que nos asegura que ha habido muchas mentes pensantes trabajando en conjunto para ofrecer la solución más extensible.

Sylius Promotion Component

Este motor de promociones se basa en el concepto de componerse de un conjunto de reglas y acciones, las cuales nos ayudan a encontrar la flexibilidad de plantear nuevas reglas de negocio y componer promociones diversas combinando estas. De tal manera, que el motor se encarga de recuperar de base de datos las promociones activadas, evaluando si nuestro pedido satisface las reglas de dichas promociones y en caso satisfactorio, aplicar las aciones correspondientes.

El componente de promociones de Sylius está diseñado para ser lo más flexible posible adaptándose a las fuertes necesidades de las campañas de marketing, como ha sucedido estos días con miles de organizaciones esta ola de transformación digital. Para ello los colaboradores de este componente, liderado en su comienzo por su fundador Paweł Jędrzejewski y actualmente por la comunidad Open Source, nos ofrecen un componente que nos ofrece funcionalidades como:

  • Promociones acotadas en el tiempo
  • Descuentos fijos y porcentuales
  • Soporte para definir y extender acciones y reglas personalizadas
  • Sistema de cupones
Interfaz que nos ofrece el Backoffice de Sylius para gestionar las promociones.

Show me the code

Lo primero, para comprender el componente, necesitamos entender los servicios de los que se compone y los puntos de entrada que nos ofrece el motor. Como vemos en la documentación, podemos contar con factorías para nuestros modelos (PromotionActionFactory), repositorios para recuperar las promociones (RepositoryPromotion) o servicios para aplicar directamente una promoción. (PromotionApplicator)

/** @var PromotionInterface $promotion */
$promotion = $this->container->get('sylius.factory.promotion')->createNew();

$promotion->setCode('discount_10%');
$promotion->setName('10% discount');

/** @var PromotionActionFactoryInterface $actionFactory */
$actionFactory = $this->container->get('sylius.factory.promotion_action');

$action = $actionFactory->createPercentageDiscount(10);

$promotion->addAction($action);

$this->container->get('sylius.repository.promotion')->add($promotion);

// and now get the PromotionApplicator and use it on an Order (assuming that you have one)
$this->container->get('sylius.promotion_applicator')->apply($order, $promotion);

Punto de entrada

Además, este servicio hace uso de tres clases imprescindibles, las cuales asumen su responsabilidad única:

  • PreQualifiedPromotionsProviderInterface, se encarga de proveer promociones candidatas a ser aplicables
  • PromotionEligibilityChecker, se ocupa de evaluar la satisfacibilidad del subject con el conjunto de reglas de la promoción
  • PromotionApplicatorInterface, se responsabiliza de poder aplicar y revertir promociones
<?php/** This file is part of the Sylius package.** (c) Paweł Jędrzejewski** 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\Component\Promotion\Processor;use Sylius\Component\Promotion\Action\PromotionApplicatorInterface;use Sylius\Component\Promotion\Checker\Eligibility\PromotionEligibilityCheckerInterface;use Sylius\Component\Promotion\Model\PromotionSubjectInterface;use Sylius\Component\Promotion\Provider\PreQualifiedPromotionsProviderInterface;final class PromotionProcessor implements PromotionProcessorInterface{    /** @var PreQualifiedPromotionsProviderInterface */    private $preQualifiedPromotionsProvider;    /** @var PromotionEligibilityCheckerInterface */    private $promotionEligibilityChecker;    /** @var PromotionApplicatorInterface */    private $promotionApplicator;    public function __construct(        PreQualifiedPromotionsProviderInterface       $preQualifiedPromotionsProvider,        PromotionEligibilityCheckerInterface $promotionEligibilityChecker,       PromotionApplicatorInterface $promotionApplicator) {     $this->preQualifiedPromotionsProvider = $preQualifiedPromotionsProvider;     $this->promotionEligibilityChecker = $promotionEligibilityChecker;     $this->promotionApplicator = $promotionApplicator;}public function process(PromotionSubjectInterface $subject): void{    foreach ($subject->getPromotions() as $promotion) {        $this->promotionApplicator->revert($subject, $promotion);    }    $preQualifiedPromotions = $this->preQualifiedPromotionsProvider->getPromotions($subject);    foreach ($preQualifiedPromotions as $promotion) {    if ($promotion->isExclusive() && $this->promotionEligibilityChecker->isEligible($subject, $promotion)) {        $this->promotionApplicator->apply($subject, $promotion);        return;     }}    foreach ($preQualifiedPromotions as $promotion) {         if (!$promotion->isExclusive() && $this-    >promotionEligibilityChecker->isEligible($subject, $promotion)) {             $this->promotionApplicator->apply($subject, $promotion);        }    }  }}

Acciones y reglas

Lo primero que contiene una promoción es un conjunto de reglas a evaluar. Una de las implementaciones por defecto que ofrece es la de evaluar si el pedido excede de cierto importe para aplicar. Implementando la interfaz RuleCheckerInterface podremos crear todas las reglas que necesitemos dado un pedido y la configuración definida para esa regla.

final class ItemTotalRuleChecker implements RuleCheckerInterface{public const TYPE = 'item_total';public function isEligible(PromotionSubjectInterface $subject, array $configuration): bool{    return $subject->getPromotionSubjectTotal() >= $configuration['amount'];}}

Para definir acciones predefinidas debemos de basarnos en la interfaz PromotionActionCommandInterface la cual debe satisfacer dos métodos, uno para aplicar una acción de la promoción y otro para revertirla.

use Sylius\Component\Promotion\Model\PromotionInterface;use Sylius\Component\Promotion\Model\PromotionSubjectInterface;interface PromotionActionCommandInterface{    public function execute(PromotionSubjectInterface $subject, array $configuration, PromotionInterface $promotion): bool;    public function revert(PromotionSubjectInterface $subject, array $configuration, PromotionInterface $promotion): void;}

De esta manera y cumpliendo con ambas interfaces podremos construir nuestras propias reglas y acciones adaptando el motor a las necesidades de negocio.

Conclusión

Como hemos podido ver en el recorrido por sus clases principales, nos encontramos patrones de diseño como Factory, Repository o Command, además de cumplir con principios SOLID, para separar responsabilidad o estar abiertos a la extensión, bases fundamentales de un framework de esta categoría.

La gran ventaja de sentirse cómodos con estos patrones de diseño es justo lo que me ha aportado para poder trasladar una solución en un lenguaje y contexto distinto a nuestra necesidad real.

Documentación

https://github.com/Sylius/Promotion

Este post está publicado en: https://adrianalonso.es/arquitectura-del-software/patrones-de-diseno/sylius-promotion-component-open-source/#

Full Stack Web Developer — adrianalonso.es

Full Stack Web Developer — adrianalonso.es