contacts_subscriptions-1.x-dev/src/Entity/Subscription.php
src/Entity/Subscription.php
<?php
namespace Drupal\contacts_subscriptions\Entity;
use Drupal\commerce_price\Price;
use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\contacts_subscriptions\Event\SubscriptionPostSaveEvent;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionableContentEntityBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the subscription entity class.
*
* @ContentEntityType(
* id = "contacts_subscription",
* label = @Translation("Subscription"),
* label_collection = @Translation("Subscriptions"),
* label_singular = @Translation("subscription"),
* label_plural = @Translation("subscriptions"),
* label_count = @PluralTranslation(
* singular = "@count subscription",
* plural = "@count subscriptions",
* ),
* bundle_label = @Translation("Subscription type"),
* handlers = {
* "storage" = "Drupal\contacts_subscriptions\Entity\SqlSubscriptionStorage",
* "view_builder" = "Drupal\contacts_subscriptions\Entity\SubscriptionViewBuilder",
* "list_builder" = "Drupal\contacts_subscriptions\Entity\SubscriptionListBuilder",
* "views_data" = "Drupal\views\EntityViewsData",
* "form" = {
* "add" = "Drupal\contacts_subscriptions\Form\SubscriptionForm",
* "edit" = "Drupal\contacts_subscriptions\Form\SubscriptionForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
* },
* "views_data" = "Drupal\contacts_subscriptions\Entity\SubscriptionViewsData",
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* }
* },
* base_table = "contacts_subscription",
* revision_table = "contacts_subscription_revision",
* show_revision_ui = TRUE,
* admin_permission = "administer subscriptions types",
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "bundle" = "bundle",
* "label" = "id",
* "uuid" = "uuid",
* "owner" = "uid"
* },
* revision_metadata_keys = {
* "revision_log_message" = "revision_log",
* "revision_created" = "revision_created",
* "revision_user" = "revision_uid"
* },
* links = {
* "add-form" = "/admin/commerce/subscriptions/add/{contacts_subscription_type}",
* "add-page" = "/admin/commerce/subscriptions/add",
* "canonical" = "/admin/commerce/subscriptions/{contacts_subscription}",
* "edit-form" = "/admin/commerce/subscriptions/{contacts_subscription}/edit",
* "delete-form" = "/admin/commerce/subscriptions/{contacts_subscription}/delete",
* "collection" = "/admin/commerce/subscriptions"
* },
* bundle_entity_type = "contacts_subscription_type",
* field_ui_base_route = "entity.contacts_subscription_type.edit_form"
* )
*/
class Subscription extends RevisionableContentEntityBase implements SubscriptionInterface {
use EntityChangedTrait;
use EntityOwnerTrait;
/**
* The statuses that are active.
*/
const STATUSES_ACTIVE = [
'active',
'cancelled_pending',
'expired_payment_pending',
'needs_payment',
];
/**
* The statuses that will automatically renew.
*/
const STATUSES_RENEWABLE = [
'active',
];
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += self::ownerBaseFieldDefinitions($entity_type);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the subscriptions was last edited.'));
$fields['status'] = BaseFieldDefinition::create('state')
->setRevisionable(TRUE)
->setRequired(TRUE)
->setLabel('Status')
->setDefaultValue('none')
->setInitialValue('none')
->setSetting('workflow', 'contacts_subscription')
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['renewal'] = BaseFieldDefinition::create('datetime')
->setRevisionable(TRUE)
->setRequired(FALSE)
->setLabel('Renewal date')
->setDescription('The date of the next membership renewal invoice.<br/><strong>Changing the renewal date can result in over or under payment at the next renewal.</strong>')
->setSetting('datetime_type', DateTimeItem::DATETIME_TYPE_DATE)
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['product'] = BaseFieldDefinition::create('entity_reference')
->setRevisionable(TRUE)
->setRequired(FALSE)
->setLabel('Product')
->setDescription('The product used for the membership.')
->setSetting('target_type', 'commerce_product')
->setSetting('handler_settings', ['target_bundles' => ['subscription' => 'subscription']])
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['price_override'] = BaseFieldDefinition::create('commerce_price')
->setName('price_override')
->setRevisionable(TRUE)
->setRequired(FALSE)
->setLabel('Price override')
->setDescription('Price to be applied for membership renewals.')
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['price_override_date'] = BaseFieldDefinition::create('datetime')
->setRevisionable(TRUE)
->setRequired(FALSE)
->setLabel('Price override expiry')
->setDescription('The date until which the discount will apply (inclusive). If not set, the price override will be perpetual.')
->setSetting('datetime_type', DateTimeItem::DATETIME_TYPE_DATE)
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['renewal_product'] = BaseFieldDefinition::create('entity_reference')
->setRevisionable(TRUE)
->setRequired(FALSE)
->setLabel('Renewal product')
->setDescription('The renewal product used for the membership.')
->setSetting('target_type', 'commerce_product')
->setSetting('handler_settings', ['target_bundles' => ['subscription' => 'subscription']])
->setDisplayConfigurable('view', TRUE)
->setDisplayConfigurable('form', TRUE);
$fields['uid']->setDisplayConfigurable('form', TRUE)
->setRequired(TRUE)
->setSetting('handler', 'search_api')
->setSetting('handler_settings', [
'index' => 'contacts_index',
]);
return $fields;
}
/**
* {@inheritDoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
$event = new SubscriptionPostSaveEvent($this, $update);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, SubscriptionPostSaveEvent::NAME);
}
/**
* {@inheritdoc}
*/
public function getStatusId(): string {
return $this->getStatusItem('status')->value;
}
/**
* {@inheritdoc}
*/
public function getStatusLabel(): ?string {
return $this->getStatusItem('status')->getLabel();
}
/**
* {@inheritdoc}
*/
public function getStatusItem(): StateItemInterface {
$items = $this->get('status');
if ($items->isEmpty()) {
$items->applyDefaultValue();
}
return $items->first();
}
/**
* {@inheritdoc}
*/
public function isActive(): bool {
return in_array($this->getStatusId(), self::STATUSES_ACTIVE);
}
/**
* {@inheritdoc}
*/
public function getExpiryDate(): ?DrupalDateTime {
if (!$this->isActive()) {
return NULL;
}
$date = (clone $this->get('renewal')->date);
if ($grace = $this->bundle->entity->getGracePeriod()) {
$date->add(new \DateInterval('P' . $grace . 'D'));
}
$date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
return $date;
}
/**
* {@inheritDoc}
*/
public function willAutoRenew(): bool {
$return = FALSE;
if (in_array($this->getStatusId(), self::STATUSES_RENEWABLE)) {
$return = $this->willRenew();
}
return $return;
}
/**
* {@inheritdoc}
*/
public function willRenew(): bool {
$return = FALSE;
if (!$this->get('renewal')->isEmpty()) {
$now = new DrupalDateTime('now');
$renewal = (clone $this->get('renewal')->date);
if ($this->bundle->entity->getRenewBeforeExpiry()) {
$days = $this->bundle->entity->getDaysBeforeExpiry();
if ($days === NULL) {
$return = TRUE;
}
else {
// Remove the number of days that a user can perform an early
// renewal at from the renewal date to bnring the renewal forward
// that number of days.
$renewal->sub(new \DateInterval('P' . $days . 'D'));
}
}
else {
// Add a day to the current time to ensure that the subscription
// won't renew on the day it is due to expire, only after.
$now->add(new \DateInterval('P1D'));
}
if (!$return) {
$now = $now->format('Y-m-d');
$renewal = $renewal->format('Y-m-d');
// If 'now' is after the renewal date, the renewal date is in the past
// and the user can renew.
$return = ($now >= $renewal);
}
}
return $return;
}
/**
* {@inheritdoc}
*/
public function getRenewalDate(bool $check_status = TRUE): ?DrupalDateTime {
if ($check_status && !$this->willRenew()) {
return NULL;
}
if ($this->get('renewal')->isEmpty()) {
return NULL;
}
return clone $this->get('renewal')->date;
}
/**
* {@inheritdoc}
*/
public function needsPaymentDetails(): bool {
return $this->isActive() && in_array($this->getStatusId(), [
'needs_payment',
'expired_payment_pending',
]);
}
/**
* {@inheritdoc}
*/
public function isCancelPending(): bool {
return $this->isActive() && $this->getStatusId() === 'cancelled_pending';
}
/**
* {@inheritdoc}
*/
public function getProduct(bool $check_status = TRUE, bool $renewal_product = FALSE): ?ProductInterface {
if ($check_status && !$this->isActive()) {
return NULL;
}
if ($renewal_product) {
if ($product = $this->get('renewal_product')->entity) {
return $product;
}
}
return $this->get('product')->entity;
}
/**
* {@inheritdoc}
*/
public function getProductId(bool $check_status = TRUE, bool $renewal_product = FALSE): ?int {
if ($check_status && !$this->isActive()) {
return NULL;
}
if ($renewal_product) {
if ($product = $this->get('renewal_product')->target_id) {
return (int) $product;
}
}
return (int) $this->get('product')->target_id ?: NULL;
}
/**
* {@inheritdoc}
*/
public function getCsrfValue(ProductVariationInterface $variation): string {
$expiry = $this->getRenewalDate();
$parts = [
'uid' => $this->getOwnerId(),
'current_status' => $this->getStatusId(),
'current_product' => $this->getProductId(),
'renewal' => $expiry ? $expiry->getTimestamp() : '',
'target_variation' => $variation->id(),
];
return implode(':', $parts);
}
/**
* {@inheritdoc}
*/
public function getOverriddenPrice(): ?Price {
// Nothing to do if there is no discount.
if ($this->get('price_override')->isEmpty()) {
return NULL;
}
// Check the expiry has not passed.
if ($this->hasOverrideExpired() === TRUE) {
return NULL;
}
return $this->get('price_override')->first()->toPrice();
}
/**
* {@inheritdoc}
*/
public function hasOverrideExpired(?DrupalDateTime $date = NULL): ?bool {
if ($this->get('price_override')->isEmpty()) {
return NULL;
}
$override_date = $this->get('price_override_date');
if ($override_date->isEmpty()) {
return TRUE;
}
if ($date === NULL) {
$date = DrupalDateTime::createFromTimestamp(\Drupal::time()->getCurrentTime());
}
$date->setDefaultDateTime();
/** @var \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem $override_date_item */
$override_date_item = $override_date->first();
/** @var \Drupal\Core\Datetime\DrupalDateTime $expiry */
$expiry = $override_date_item->date;
return $date->getTimestamp() > $expiry->getTimestamp();
}
/**
* {@inheritdoc}
*/
public function canChangeProduct() : bool {
if ($this->get('renewal_product')->target_id) {
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isChanging() : bool {
if ($renewal_product = $this->get('renewal_product')->target_id) {
if ($renewal_product != $this->get('product')->target_id) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public static function sort(SubscriptionInterface $a, SubscriptionInterface $b): int {
// Sort by is renewable, then is active, then by closest expiry.
return $b->willRenew() <=> $a->willRenew() ?:
$b->isActive() <=> $a->isActive() ?:
// Closest expiry is the lower number.
self::getSortTimestamp($b) <=> self::getSortTimestamp($a);
}
/**
* Wrapper to get a timestamp without triggering errors.
*
* @param \Drupal\contacts_subscriptions\Entity\SubscriptionInterface $subscription
* The subscription.
*
* @return int
* The sortable timestamp.
*/
public static function getSortTimestamp(SubscriptionInterface $subscription): int {
$date = $subscription->get('renewal')->date;
return $date ? $date->getTimestamp() : 0;
}
}
