Skip to content

Instantly share code, notes, and snippets.

@clrockwell
Last active June 27, 2018 20:50
Show Gist options
  • Save clrockwell/397c20bc270451cd11ccc34c2a53984e to your computer and use it in GitHub Desktop.
Save clrockwell/397c20bc270451cd11ccc34c2a53984e to your computer and use it in GitHub Desktop.
Provide a prorated discount for license roles; useful in upsells when someone already has purchased role, they will receive a discount equal to the amount of time left.
<?php
namespace Drupal\commerce_promotion_prorated\Plugin\Commerce\PromotionOffer;
use Drupal\commerce_license\Entity\License;
use Drupal\commerce_order\Adjustment;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_price\Price;
use Drupal\commerce_product\Entity\ProductAttributeValue;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\commerce_promotion\Entity\PromotionInterface;
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferBase;
use Drupal\commerce_promotion_prorated\LegacyService;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\openid_connect\Authmap;
use Drupal\platform_connector\ConnectionService;
use Drupal\platform_connector\SubscriptionUtility;
use Drupal\scholarrx_commerce_promotions\Plugin\Commerce\Condition\ClientProduct;
use GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the prorated percentage for license orders.
*
* @CommercePromotionOffer(
* id = "license_prorated_offer",
* label = @Translation("Prorated discount"),
* entity_type = "commerce_order_item",
* )
*/
class LicenseProrated extends PromotionOfferBase {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $licenseStorage;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $orderItemStorage;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Drupal\platform_connector\ConnectionService definition.
*
* @var \Drupal\platform_connector\ConnectionService
*/
protected $connectionService;
/**
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* @var \Drupal\openid_connect\Authmap
*/
protected $authmap;
/**
* @var \Drupal\commerce_promotion_prorated\LegacyService
*/
protected $legacyService;
public function __construct(array $configuration, $plugin_id, $plugin_definition, \Drupal\commerce_price\RounderInterface $rounder, EntityTypeManagerInterface $entityTypeManager, AccountInterface $user, ConnectionService $connectionService, TimeInterface $time, Authmap $authmap, LegacyService $legacyService) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $rounder);
$this->licenseStorage = $entityTypeManager->getStorage('commerce_license');
$this->orderItemStorage = $entityTypeManager->getStorage('commerce_order_item');
$this->currentUser = $user;
$this->time = $time;
$this->connectionService = $connectionService;
$this->authmap = $authmap;
$this->legacyService = $legacyService;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('commerce_price.rounder'),
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('platform_connector.connection'),
$container->get('datetime.time'),
$container->get('openid_connect.authmap'),
$container->get('commerce_promotion_prorated.legacy')
);
}
public function apply(EntityInterface $entity, PromotionInterface $promotion) {
if ($this->currentUser->isAnonymous()) {
return;
}
$this->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $entity;
// The roles this condition allows for
$modules = $this->getModules($promotion);
// If there are no modules defined, return.
// @todo maybe some logging here, if an editor is using this discount they
// should have defined modules it is applicable to.
if (empty($modules)) {
return;
}
$sub = $this->authmap->getConnectedAccounts($order_item->getOrder()
->getCustomer())['generic'];
if (!$sub) {
return;
}
$eligible_modules = $this->getEligibleModules($modules, $sub);
foreach ($eligible_modules as $module) {
$discount_percentage = $this->getSubLeftPercentage($module);
$license = $this->getLicenseForModule($module);
if ($license) {
$prorate_amount = $this->calculateLicenseProratedAmount($license, $order_item, $discount_percentage);
}
else {
$prorate_amount = $this->calculateLegacyProratedAmount($module, $order_item, $discount_percentage);
}
if (!isset($prorate_amount) || !$prorate_amount) {
continue;
}
$order_item->addAdjustment(new Adjustment([
'type' => 'promotion',
// @todo Change to label from UI when added in #2770731.
'label' => $promotion->getDisplayName(),
'amount' => $prorate_amount->multiply('-1'),
'source_id' => $promotion->id(),
]));
}
}
protected function assertSub() {
}
protected function getModules(PromotionInterface $promotion) {
$conditions = $promotion->getConditions();
$modules = [];
foreach ($conditions as $condition) {
if ($condition instanceof ClientProduct) {
$modules = $condition->getConfiguration()['modules'];
}
}
return $modules;
}
protected function getEligibleModules($modules, $sub) {
$eligible_modules = [];
$subscriptions = $this->connectionService->getAccountRemoteSubscriptionsBySub($sub);
foreach ($modules as $role) {
if ($active = SubscriptionUtility::userHasActiveSubscriptionForModule($subscriptions, ConnectionService::mapRolesToPlatform($role))) {
$eligible_modules[] = $active;
}
}
return $eligible_modules;
}
protected function getLicenseForModule($module) {
$license = $this->licenseStorage->loadByProperties([
'platform_remote_id' => $module['id'],
]);
// Two licenses can have the same remote ID, we want the most recent
if (is_array($license)) {
$license = array_pop($license);
}
return $license ?: FALSE;
}
protected function getSubLeftPercentage($module) {
// How much time was subscription for
$sub_length = strtotime($module['endDate'] . " UTC") - strtotime($module['beginDate'] . " UTC");
// How much time is left
$sub_left = strtotime($module['endDate']) - $this->time->getRequestTime();
if ($sub_left < 0) {
return 0;
}
return (($sub_left / $sub_length));
}
protected function getSubLeftPercentageUsingLegacy($legacyItem) {
// How much time was subscription for
$sub_length = $legacyItem['endDate'] - $legacyItem['beginDate'];
// How much time is left
$sub_left = $legacyItem['endDate'] - $this->time->getRequestTime();
if ($sub_left < 0) {
return 0;
}
// What is discount percentage
return (($sub_left / $sub_length));
}
/**
* Calculate prorated amount based on a subscription from legacy.
* @param $module
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* @param $discount_percentage
* @return Price|bool
*/
protected function calculateLegacyProratedAmount($module, OrderItemInterface $order_item, $discount_percentage) {
$email = $order_item->getOrder()->getEmail();
$legacyItem = $this->legacyService->getLegacySubscriptionForEmailAndProduct($email, $module['product']);
if (!$legacyItem) return FALSE;
$legacyItemFubar = strpos($legacyItem, '{');
$legacyItem = substr($legacyItem, $legacyItemFubar);
$legacyItem = Json::decode($legacyItem);
if (array_key_exists('error', $legacyItem)) {
\Drupal::logger('license_prorate')
->alert('Error from legacy: ' . $legacyItem['error']);
return FALSE;
}
// Client 2.0 has an issue with start dates being in 1970 so we need to use legacy api to estimate
$discount_percentage = $this->getSubLeftPercentageUsingLegacy($legacyItem);
/** @var Price $license_to_prorate_cost */
$license_to_prorate_cost = new Price((string) $legacyItem['product_gross'], 'USD');
if ($legacyItem['discount']) {
$license_to_prorate_cost->subtract(new Price((string) $legacyItem['discount'], 'USD'));
}
$prorate_amount = $license_to_prorate_cost->multiply((string) $discount_percentage);
$prorate_amount = $this->rounder->round($prorate_amount);
// Never discount more than the total price of the order_item.
// @todo does this re-calc when coupons are added?
$unit_price = $order_item->getAdjustedUnitPrice();
if ($prorate_amount->greaterThan($unit_price)) {
$prorate_amount = $unit_price;
}
return $prorate_amount;
}
protected function legacyItemIsLongerThanPurchasedItem($legacy_item, OrderItemInterface $orderItem) {
// return 3, 6, 18, etc.
$newItemLength = $this->getLengthForOrderItem($orderItem);
$oldItemLength = $this->legacyService->getLengthForLegacyItem($legacy_item);
\Drupal::logger('license_proate')
->alert('New Item Length: ' . $newItemLength . '; Old Item Length: ' . $oldItemLength);
return $newItemLength >= $oldItemLength;
}
/**
* Calculate prorated amount based on a License that was
* purchased in 2.0.
*
* @param License $license
* @param $order_item
* @param $discount_percentage
*
* @return Price|bool
*/
protected function calculateLicenseProratedAmount(License $license, OrderItemInterface $order_item, $discount_percentage) {
// How much did they pay for this license?
$license_order_item = $this->orderItemStorage->loadByProperties([
'license' => $license->id(),
]);
// If the license wasn't purchased.
if (empty($license_order_item)) {
return FALSE;
}
/** @var OrderItemInterface $license_to_prorate_order_item */
$license_to_prorate_order_item = reset($license_order_item);
// PL2-1027
if (!$this->newItemIsEqualOrLongerThanOldItem($order_item, $license_to_prorate_order_item)) {
return FALSE;
}
/** @var Price $license_to_prorate_cost */
$license_to_prorate_cost = $license_to_prorate_order_item->getAdjustedUnitPrice();
$prorate_amount = $license_to_prorate_cost->multiply((string) $discount_percentage);
$prorate_amount = $this->rounder->round($prorate_amount);
// Never discount more than the total price of the order_item.
// @todo does this re-calc when coupons are added?
$unit_price = $order_item->getAdjustedUnitPrice();
if ($prorate_amount->greaterThan($unit_price)) {
$prorate_amount = $unit_price;
}
return $prorate_amount;
}
/**
* PL2-1027 Hat tip @EmilyByrnes for figuring this out.
*
* We do not allow a user to receive a pro-rated discount if purchasing
* an item that is of lesser length. The reason for this is that eComm
* currently does not have a method of altering subscriptions *except* for
* addTime.
*
* As a result a user could purchase a 24 month subscription, upgrade to a 12
* month 360, get their pro-rated discount and end up with 12 months FF and
* EXP but still have 24 months of Qmax.
*
* Therefore, don't apply the pro-rated discount if the length is less :)
*
* @param \Drupal\commerce_order\Entity\OrderItem $newItem
* @param \Drupal\commerce_order\Entity\OrderItem $oldItem
* @return bool
*/
protected function newItemIsEqualOrLongerThanOldItem(OrderItem $newItem, OrderItem $oldItem) {
$newItemLength = $this->getLengthForOrderItem($newItem);
$oldItemLength = $this->getLengthForOrderItem($oldItem);
return $newItemLength >= $oldItemLength;
}
protected function getLengthForOrderItem(OrderItem $orderItem) {
/** @var ProductVariation $variation */
$variation = $orderItem->getPurchasedEntity();
/** @var ProductAttributeValue $length_attribute */
$length_attribute = $variation->getAttributeValue('attribute_length');
return $length_attribute->label();
}
protected function hasEnoughTimeLeft($end_date_string) {
// PL2-1017 - remove the restriction on 90 days
return TRUE;
// Woot
$now = new \DateTime();
$now->setTimezone(new \DateTimeZone('UTC'));
$end = new \DateTime($end_date_string);
$interval = $end->diff($now);
$days_diff = $interval->format('%a');
return (int) $days_diff > 90;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment