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
Como suele decir la generación pasada, “la experiencia es un grado”. Más allá de dedicarse al desarrollo software y haber podido trabajar con diferentes tecnologías, considero que lo más importante es el haberse enfrentado a problemáticas distintas y saber adaptar el conocimiento adquirido para resolver nuevos problemas. En este punto, me gustaría recomendar este post acerca de lo que significa tener una verdadera experiencia laboral: https://dpersonas.com/2017/01/31/sirve-de-algo-la-experiencia-o-en-estos/
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
Uno de los componentes construidos en esta comunidad es el Promotion Component y su diseño interno es el que nos ha inspirado para construir nuestra solución, en un lenguaje y contexto distinto, pero con los mismos objetivos. Es por ello que en este post hablaré en profundidad de las bondades de este componente que nos ha permitido construir nuestro propio motor de promociones.
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 en 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 con funcionalidades como:
- Promociones acotadas en el tiempo
- Descuentos fijos y porcentuales
- Soporte para definir y extender acciones y reglas personalizadas
- Sistema de cupones
Show me the code
Pero más allá de lo conceptual vamos a ver parte del código del componente en el que merece la pena pararse a leer y analizar.
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** @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
Mas allá de ver clases independientes con una responsabilidad clara, necesitamos encontrar un punto de entrada que orqueste estos servicios. Buceando por el repositorio nos encontramos el servicio PromotionProcessor el cual contiene un método process que recibe una interfaz PromotionSubjectInterface, la cual puede representar un hipotético pedido. Esta clase es candidato a ser nuestro punto de entrada. Como podemos ver en el siguiente código, esta interfaz no se acopla totalmente al concepto de pedido, sino a un elemento que nos ofrece importes y una colección de promociones.
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<?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
Si seguimos profundizando en el repositorio encontramos dos carpetas esenciales Action y Checker, las cuales contienen las clases necesarias para manejar las acciones y las reglas respectivamente.
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.
1 2 3 4 5 6 7 8 9 |
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.
1 2 3 4 5 6 7 8 9 10 11 |
namespace Sylius\Component\Promotion\Action; 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
Una de las grandes ventajas de Sylius respecto a soluciones e-commerce completas como Magento o Woocomerce que llevan más tiempo en el mercado es el nivel de calidad del software implementado. Sylius está construido con las mejores prácticas, utilizando patrones de diseño que permiten cumplir con esa developer experience, pudiendo personalizar y adaptar la base software que nos ofrece a las necesidades reales del negocio con un time to market muy bajo.
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://docs.sylius.com/en/latest/book/orders/promotions.html