<?php
declare(strict_types=0);
/*
* WellCommerce Foundation
*
* This file is part of the WellCommerce package.
*
* (c) Adam Piotrowski <adam@wellcommerce.org>, Adrian Potepa <adrian@wellcommerce.org>
*
* For the full copyright and license information,
* please view the LICENSE file that was distributed with this source code.
*/
namespace UniSport\Bundle\AppBundle\EventListener;
use KlaviyoAPI\API\EventsApi;
use KlaviyoAPI\API\ListsApi;
use KlaviyoAPI\API\ProfilesApi;
use KlaviyoAPI\KlaviyoAPI;
use KlaviyoAPI\Model\EventCreateQueryV2;
use KlaviyoAPI\Model\ListMembersAddQuery;
use KlaviyoAPI\Model\ProfileUpsertQuery;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use WellCommerce\Bundle\AppBundle\Entity\AddressInterface;
use WellCommerce\Bundle\AppBundle\Entity\ClientBillingAddress;
use WellCommerce\Bundle\AppBundle\Entity\ClientContactDetails;
use WellCommerce\Bundle\AppBundle\Entity\Media;
use WellCommerce\Bundle\AppBundle\Entity\NewsletterSubscriber;
use WellCommerce\Bundle\AppBundle\Entity\Shop;
use WellCommerce\Bundle\CatalogBundle\Entity\Producer;
use WellCommerce\Bundle\CoreBundle\DependencyInjection\AbstractServiceSubscriber;
use WellCommerce\Bundle\CoreBundle\Doctrine\Event\EntityEvent;
use WellCommerce\Bundle\OrderBundle\DataSet\Admin\OrderStatusDataSet;
use WellCommerce\Bundle\OrderBundle\Entity\Order;
use WellCommerce\Bundle\OrderBundle\Entity\OrderProduct;
use WellCommerce\Bundle\OrderBundle\Entity\OrderStatus;
use WellCommerce\Bundle\OrderBundle\Entity\OrderStatusHistory;
use WellCommerce\Component\Form\Event\FormEvent;
/**
* Class KlaviyoSubscriber
*
* @author Adam Piotrowski <adam@wellcommerce.org>
*/
class KlaviyoSubscriber extends AbstractServiceSubscriber implements EventSubscriberInterface
{
protected OrderStatusDataSet $dataSet;
protected ?KlaviyoAPI $api = null;
public function __construct(ContainerInterface $locator, OrderStatusDataSet $dataSet)
{
parent::__construct($locator);
$this->dataSet = $dataSet;
}
public static function getSubscribedEvents(): array
{
return [
'order.post_confirm' => 'onOrderPostConfirm',
'admin.shop.pre_form_init' => ['onShopFormAdminInit'],
'order_status_history.post_create' => ['onOrderStatusHistoryPostCreate', 0],
'newsletter_subscriber.post_create' => ['onNewsletterSubscriberPostCreate', 0],
];
}
public function onShopFormAdminInit(FormEvent $event)
{
$resource = $event->getResource();
if ($resource instanceof Shop) {
$form = $event->getForm();
$builder = $event->getFormBuilder();
$klaviyoData = $form->addChild(
$builder->getElement('nested_fieldset', [
'name' => 'klaviyo_data',
'label' => 'klaviyo.fieldset.settings',
])
);
$klaviyoData->addChild(
$builder->getElement('text_field', [
'name' => 'klaviyoApiPublicKey',
'label' => 'klaviyo.label.public_key',
])
);
$klaviyoData->addChild(
$builder->getElement('password', [
'name' => 'klaviyoApiPrivateKey',
'label' => 'klaviyo.label.private_key',
])
);
$klaviyoData->addChild(
$builder->getElement('text_field', [
'name' => 'klaviyoMainListId',
'label' => 'klaviyo.label.main_list_id',
])
);
$klaviyoData->addChild(
$builder->getElement('checkbox', [
'name' => 'klaviyoApiEnabled',
'label' => 'klaviyo.label.enabled',
])
);
$orderStatuses = $this->dataSet->getResult('select', [], ['default_option' => '---']);
$klaviyoData->addChild(
$builder->getElement('select', [
'name' => 'klaviyoStatusOrderFulfilled',
'label' => 'klaviyo.label.order_status_fulfilled',
'options' => $orderStatuses,
'transformer' => $builder->getRepositoryTransformer('entity', OrderStatus::class),
])
);
$klaviyoData->addChild(
$builder->getElement('select', [
'name' => 'klaviyoStatusOrderCancelled',
'label' => 'klaviyo.label.order_status_cancelled',
'options' => $orderStatuses,
'transformer' => $builder->getRepositoryTransformer('entity', OrderStatus::class),
])
);
$klaviyoData->addChild(
$builder->getElement('select', [
'name' => 'klaviyoStatusOrderRefunded',
'label' => 'klaviyo.label.order_status_refunded',
'options' => $orderStatuses,
'transformer' => $builder->getRepositoryTransformer('entity', OrderStatus::class),
])
);
}
}
public function onOrderPostConfirm(EntityEvent $event)
{
$order = $event->getEntity();
if ($order instanceof Order) {
$shop = $order->getShop();
if ($shop->isKlaviyoApiEnabled()) {
$this->sendStatusPlaced($shop, $order);
}
}
}
public function onNewsletterSubscriberPostCreate(EntityEvent $event)
{
/** @var NewsletterSubscriber $newsletterSubscriber */
$newsletterSubscriber = $event->getEntity();
if ($newsletterSubscriber instanceof NewsletterSubscriber) {
$shop = $newsletterSubscriber->getShop();
$listId = $shop->getKlaviyoMainListId();
if ($shop->isKlaviyoApiEnabled() && !empty($listId)) {
$this->sendListSubscribe($shop, $listId, $newsletterSubscriber->getEmail());
}
}
}
public function onOrderStatusHistoryPostCreate(EntityEvent $event)
{
$history = $event->getEntity();
if ($history instanceof OrderStatusHistory) {
$order = $history->getOrder();
$shop = $order->getShop();
$status = $history->getOrderStatus();
if ($shop->isKlaviyoApiEnabled()) {
$statusFulfilled = $shop->getKlaviyoStatusOrderFulfilled();
if ($statusFulfilled instanceof OrderStatus) {
if ($status->getId() === $statusFulfilled->getId()) {
$this->sendOrderStatus($shop, $order, 'Fulfilled', 'Fulfilled Order');
}
}
$statusCancelled = $shop->getKlaviyoStatusOrderCancelled();
if ($statusCancelled instanceof OrderStatus) {
if ($status->getId() === $statusCancelled->getId()) {
$this->sendOrderStatus($shop, $order, 'Cancelled', 'Cancelled Order');
}
}
$statusRefunded = $shop->getKlaviyoStatusOrderRefunded();
if ($statusRefunded instanceof OrderStatus) {
if ($status->getId() === $statusRefunded->getId()) {
$this->sendOrderStatus($shop, $order, 'Refunded', 'Refunded Order');
}
}
}
}
}
private function getApiClient(Shop $shop): KlaviyoAPI
{
if (!$this->api instanceof KlaviyoAPI) {
$this->api = new KlaviyoAPI($shop->getKlaviyoApiPrivateKey());
}
return $this->api;
}
private function sendListSubscribe(Shop $shop, $listId, $email)
{
/** @var ListsApi $api */
$api = $this->getApiClient($shop)->Lists;
$profile = $this->getProfile($shop, $email);
if (null === $profile) {
return;
}
$data = [];
$data[] = [
'type' => 'profile',
'id' => $profile,
];
$query = new ListMembersAddQuery(['data' => $data]);
try {
$api->createListRelationships($listId, $query);
} catch (\Throwable $ex) {
if ($this->getKernel()->isDebug()) {
echo $ex->getMessage();
die();
}
}
}
private function getProfile(Shop $shop, string $email): ?string
{
/** @var ProfilesApi $api */
$api = $this->getApiClient($shop)->Profiles;
$query = new ProfileUpsertQuery([
'data' => [
'type' => 'profile',
'attributes' => [
'email' => $email,
],
],
]);
try {
$response = $api->createOrUpdateProfile($query);
return $response['data']['id'] ?? null;
} catch (\Throwable $throwable) {
return null;
}
}
private function sendStatusPlaced(Shop $shop, Order $order)
{
$this->sendOrderStatus($shop, $order, 'Placed', 'Placed Order');
foreach ($order->getProducts() as $orderProduct) {
$this->sendOrderedProduct($shop, $order, $orderProduct);
}
}
private function sendOrderedProduct(Shop $shop, Order $order, OrderProduct $orderProduct)
{
/** @var EventsApi $api */
$api = $this->getApiClient($shop)->Events;
$contactDetails = $order->getContactDetails();
$billingAddress = $order->getBillingAddress();
$properties = $this->prepareProduct($orderProduct);
$product = $orderProduct->getProduct();
$properties['Reason'] = 'Ordered Product';
$properties['$event_id'] = $order->getId() . '_' . $product->getSku();
$properties['$value'] = $orderProduct->getSellPrice()->getGrossAmount();
$data = [
'type' => 'event',
'attributes' => [
'properties' => $properties,
'value' => $order->getSummary()->getGrossAmount(),
'value_currency' => $order->getCurrency(),
'metric' => [
'data' => [
'type' => 'metric',
'attributes' => [
'name' => 'Ordered Product',
],
],
],
'profile' => $this->prepareProfile($billingAddress, $contactDetails),
],
];
$params['data'] = $data;
$query = new EventCreateQueryV2($params);
try {
$api->createEvent($query);
} catch (\Throwable $throwable) {
if ($this->getKernel()->isDebug()) {
echo $throwable->getMessage();
die();
}
}
}
private function sendOrderStatus(Shop $shop, Order $order, string $reason, string $metric)
{
/** @var EventsApi $api */
$api = $this->getApiClient($shop)->Events;
$contactDetails = $order->getContactDetails();
$billingAddress = $order->getBillingAddress();
$properties = $this->prepareOrderProperties($order);
$properties['Reason'] = $reason;
$data = [
'type' => 'event',
'attributes' => [
'properties' => $properties,
'value' => $order->getSummary()->getGrossAmount(),
'value_currency' => $order->getCurrency(),
'metric' => [
'data' => [
'type' => 'metric',
'attributes' => [
'name' => $metric,
],
],
],
'profile' => $this->prepareProfile($billingAddress, $contactDetails),
],
];
$params['data'] = $data;
$query = new EventCreateQueryV2($params);
try {
$api->createEvent($query);
} catch (\Throwable $throwable) {
if ($this->getKernel()->isDebug()) {
echo $throwable->getMessage();
die();
}
}
}
private function prepareItems(Order $order): array
{
$items = [];
foreach ($order->getProducts() as $orderProduct) {
$items[] = $this->prepareProduct($orderProduct);
}
return $items;
}
private function prepareProduct(OrderProduct $orderProduct): array
{
$product = $orderProduct->getProduct();
$categories = [];
foreach ($product->getCategories() as $category) {
$categories[] = $category->translate()->getName();
}
$image = '';
if ($product->getPhoto() instanceof Media) {
$image = $this->getImageHelper()->getImage($product->getPhoto()->getPath(), 'original');
}
return [
'ProductID' => $product->getId(),
'SKU' => $product->getSku(),
'ProductName' => $product->translate()->getName(),
'Quantity' => $orderProduct->getQuantity(),
'ItemPrice' => $orderProduct->getSellPrice()->getGrossAmount(),
'RowTotal' => round($orderProduct->getSellPrice()->getGrossAmount() * $orderProduct->getQuantity(), 2),
'ProductURL' => $this->getRouterHelper()->generateUrl($product->translate()->getRoutePath()),
'ImageURL' => $image,
'Categories' => $categories,
'Brand' => $product->getProducer() instanceof Producer ? $product->getProducer()->translate()->getName() : '',
];
}
private function getOrderCategories(Order $order): array
{
$items = [];
foreach ($order->getProducts() as $orderProduct) {
$product = $orderProduct->getProduct();
foreach ($product->getCategories() as $category) {
$items[] = $category->translate()->getName();
}
}
return array_unique($items);
}
private function getOrderItemNames(Order $order): array
{
$items = [];
foreach ($order->getProducts() as $orderProduct) {
$product = $orderProduct->getProduct();
$items[] = $product->translate()->getName();
}
return $items;
}
private function getOrderBrands(Order $order): array
{
$items = [];
foreach ($order->getProducts() as $orderProduct) {
$product = $orderProduct->getProduct();
$producer = $product->getProducer();
if ($producer instanceof Producer) {
$items[] = $producer->translate()->getName();
}
}
return array_unique($items);
}
private function prepareAddress(AddressInterface $address, ClientContactDetails $contactDetails): array
{
return [
'FirstName' => $address->getFirstName(),
'LastName' => $address->getLastName(),
'Company' => $address->getCompanyName(),
'Address1' => $address->getLine1(),
'Address2' => trim($address->getLine2() . ' ' . $address->getLine3()),
'City' => $address->getCity(),
'Region' => $address->getState(),
'RegionCode' => $address->getState(),
'Country' => $address->getCountry(),
'CountryCode' => $address->getCountry(),
'Zip' => $address->getPostalCode(),
'Phone' => $contactDetails->getPhone(),
];
}
private function prepareProfile(ClientBillingAddress $billingAddress, ClientContactDetails $contactDetails): array
{
return [
'data' => [
'type' => 'profile',
'attributes' => [
'phone_number' => '+48' . $contactDetails->getPhone(),
'email' => $contactDetails->getEmail(),
'first_name' => $contactDetails->getFirstName(),
'last_name' => $contactDetails->getLastName(),
'organization' => $billingAddress->getCompanyName(),
'location' => [
'address1' => $billingAddress->getLine1(),
'address2' => trim($billingAddress->getLine2() . ' ' . $billingAddress->getLine3()),
'city' => $billingAddress->getCity(),
'country' => $billingAddress->getCountry(),
'region' => $billingAddress->getState(),
'zip' => $billingAddress->getPostalCode(),
],
'properties' => [
'$email' => $contactDetails->getEmail(),
'$first_name' => $contactDetails->getFirstName(),
'$last_name' => $contactDetails->getLastName(),
'$phone_number' => $contactDetails->getPhone(),
'$address1' => $billingAddress->getLine1(),
'$address2' => trim($billingAddress->getLine2() . ' ' . $billingAddress->getLine3()),
'$city' => $billingAddress->getCity(),
'$zip' => $billingAddress->getPostalCode(),
'$region' => $billingAddress->getState(),
'$country' => $billingAddress->getCountry(),
],
],
],
];
}
private function prepareOrderProperties(Order $order): array
{
$contactDetails = $order->getContactDetails();
$billingAddress = $order->getBillingAddress();
$shippingAddress = $order->getShippingAddress();
$discountCode = '';
$discountValue = 0;
if ($order->hasModifier('coupon_discount')) {
$discountValue = $order->getModifier('coupon_discount')->getGrossAmount();
$discountCode = $order->getCoupon()->getCode();
}
return [
'$event_id' => $order->getId(),
'$value' => $order->getSummary()->getGrossAmount(),
'OrderId' => $order->getId(),
'Categories' => $this->getOrderCategories($order),
'ItemNames' => $this->getOrderItemNames($order),
'Brands' => $this->getOrderBrands($order),
'DiscountCode' => $discountCode,
'DiscountValue' => $discountValue,
'Items' => $this->prepareItems($order),
'BillingAddress' => $this->prepareAddress($billingAddress, $contactDetails),
'ShippingAddress' => $this->prepareAddress($shippingAddress, $contactDetails),
];
}
}