contacts_events-8.x-1.x-dev/src/Form/TransferForm.php
src/Form/TransferForm.php
<?php
namespace Drupal\contacts_events\Form;
use CommerceGuys\Intl\Formatter\CurrencyFormatterInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_payment\Entity\PaymentGateway;
use Drupal\commerce_price\Price;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\contacts_events\Entity\EventClass;
use Drupal\contacts_events\Entity\TicketInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Entity form variant for transferring a ticket.
*/
class TransferForm extends ContentEntityForm {
/**
* The ticket to be transferred.
*
* @var \Drupal\contacts_events\Entity\TicketInterface
*/
protected $entity;
/**
* The booking to transfer a ticket to.
*
* @var \Drupal\commerce_order\Entity\OrderInterface
*/
protected $targetBooking;
/**
* The storage for Events Classes.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $classStorage;
/**
* The storage for Commerce Bookings.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $orderStorage;
/**
* The storage for Commerce Payments.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $paymentStorage;
/**
* The currency formatter service.
*
* @var \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface
*/
protected $currencyFormatter;
/**
* TransferForm constructor.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entity_type_manager
* The entity type manager service.
* @param \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface|null $currency_formatter
* The currency formatter service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, EntityTypeManagerInterface $entity_type_manager = NULL, CurrencyFormatterInterface $currency_formatter = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$entity_type_manager = $entity_type_manager ?: \Drupal::service('entity_type.manager');
$this->classStorage = $entity_type_manager->getStorage('contacts_events_class');
$this->orderStorage = $entity_type_manager->getStorage('commerce_order');
$this->paymentStorage = $entity_type_manager->getStorage('commerce_payment');
$this->currencyFormatter = $currency_formatter ?: \Drupal::service('commerce_price.currency_formatter');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('entity_type.manager'),
$container->get('commerce_price.currency_formatter')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Show the booking step if we don't have a target booking.
if (!$this->targetBooking) {
$form = $this->bookingForm($form, $form_state);
}
// Otherwise show the confirmation step.
else {
$form = $this->confirmationForm($form, $form_state);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Only validate the booking ID if step one is being submitted.
if (!$this->targetBooking) {
$event_code = $this->entity->getEvent()->get('code')->value;
$booking_id = $form_state->getValue('booking');
// Remove the 0-padding to get the Order ID.
$booking_id = ltrim($booking_id, '0');
$booking = $this->orderStorage->load($booking_id);
// Validate that the order exists.
if (!$booking) {
$form_state->setError($form['booking'], $this->t('Booking %booking does not exist.', ['%booking' => $event_code . '-' . $booking_id]));
}
// And is a booking.
elseif ($booking->bundle() != 'contacts_booking') {
$form_state->setError($form['booking'], $this->t('Order %booking is not a Booking.', ['%booking' => $booking_id]));
}
// And that the booking is for the same event.
elseif ($booking->get('event')->target_id != $this->entity->getOrderItem()->getOrder()->get('event')->target_id) {
$form_state->setError($form['booking'], $this->t('Booking %booking is for a different event to this booking.', ['%booking' => $booking->get('event')->entity->get('code')->value . '-' . $booking->id()]));
}
// And that the booking is not the source booking.
elseif ($booking->id() == $this->entity->getOrderItem()->getOrderId()) {
$form_state->setError($form['booking'], $this->t('You cannot transfer tickets to the same booking.'));
}
// Check whether we can calculate the amount paid on an Order Item.
if ($form_state->getValue('transfer_payment') && is_null($this->getPaidForItem($this->entity->getOrderItem()))) {
$form_state->setError($form['transfer_payment'], $this->t('Payments cannot be automatically transferred for this Ticket.'));
}
// If there are no errors set the target booking and rebuild the form.
if (empty($form_state->getErrors())) {
$this->targetBooking = $booking;
$form_state->setRebuild(TRUE);
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Get the amount paid before any changes are made.
$paid = $this->getPaidForItem($this->entity->getOrderItem());
// Move order item to target booking.
$order_item = $this->entity->getOrderItem();
$this->targetBooking->addItem($order_item);
// Remove order item from original booking.
$original_booking = $order_item->getOrder();
$original_booking->removeItem($order_item);
// Update the Order ID on the Order Item.
$order_item->set('order_id', $this->targetBooking->id());
$order_item->save();
// Append a note to booking notes on both orders.
$replacements = [
'%time' => $this->time->getRequestTime(),
'%ticket_holder' => $this->entity->getName(),
'%original_booking' => $original_booking->label(),
'%target_booking' => $this->targetBooking->label(),
'%user' => $this->currentUser()->getDisplayName(),
'%uid' => $this->currentUser()->id(),
];
$transfer_message = $this->t("<br/>%time: Ticket for %ticket_holder transferred from %original_booking to %target_booking by %user [%uid].", $replacements);
$target_notes = $this->targetBooking->get('back_end_notes')->value;
$target_notes .= $transfer_message;
$this->targetBooking->set('back_end_notes', $target_notes);
$this->targetBooking->save();
$original_notes = $original_booking->get('back_end_notes')->value;
$original_notes .= $transfer_message;
$original_booking->set('back_end_notes', $original_notes);
$original_booking->save();
// Show user a confirmation message.
$replacements = [
'%ticket_holder' => $this->entity->getName(),
'%old_booking' => $original_booking->label(),
'%new_booking' => $this->targetBooking->label(),
];
$this->messenger()->addMessage($this->t('Ticket for %ticket_holder transferred from %old_booking to %new_booking', $replacements));
// If transfer payments is selected, create payments to transfer the amount
// paid on the order item.
if ($form_state->getValue('transfer_payment')) {
// Only transfer payments if we can calculate the amount paid for the
// Order Item and the transfer payment gateway exists.
$transfer_payment_gateway = PaymentGateway::load('booking_transfer');
if ($paid && $paid->isPositive() && $transfer_payment_gateway) {
// Add a Booking Transfer payment to target booking for paid value.
/** @var \Drupal\commerce_payment\Entity\PaymentInterface $target_booking_payment */
$target_booking_payment = $this->paymentStorage->create([
'state' => 'completed',
'amount' => $paid,
'payment_gateway' => 'booking_transfer',
'order_id' => $this->targetBooking->id(),
'completed' => $this->time->getRequestTime(),
'order_item_tracking' => [
$order_item->id() => $paid->toArray() + ['target_id' => $order_item->id()],
],
]);
$target_booking_payment->save();
// Add Booking Transfer payment to original booking to negate the
// amount that has been paid on that order.
$paid = $paid->multiply('-1');
/** @var \Drupal\commerce_payment\Entity\PaymentInterface $source_booking_payment */
$source_booking_payment = $this->paymentStorage->create([
'state' => 'completed',
'amount' => $paid,
'payment_gateway' => 'booking_transfer',
'order_id' => $original_booking,
'completed' => $this->time->getRequestTime(),
'order_item_tracking' => [
$order_item->id() => $paid->toArray() + ['target_id' => $order_item->id()],
],
]);
$source_booking_payment->save();
// Show user a message about the transferred payment after un-negating.
$paid = $paid->multiply('-1');
$replacements = [
'%payment_value' => $this->currencyFormatter->format($paid->getNumber(), $paid->getCurrencyCode()),
'%old_booking' => $original_booking->label(),
'%new_booking' => $this->targetBooking->label(),
];
$this->messenger()->addMessage($this->t('Payment of for %payment_value transferred from %old_booking to %new_booking', $replacements));
}
}
// Redirect to the target booking to see the newly transferred ticket.
$form_state->setRedirect('entity.commerce_order.booking_admin_tickets', ['commerce_order' => $this->targetBooking->id()]);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['continue'] = [
'#type' => 'submit',
'#value' => $this->targetBooking ? $this->t('Transfer booking') : $this->t('Continue'),
'#submit' => ['::submitForm'],
];
return $actions;
}
/**
* Create the booking step form for transferring a ticket.
*
* @param array $form
* The current form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A Form API form for booking step of the ticket transfer.
*/
protected function bookingForm(array $form, FormStateInterface $form_state) {
$form['ticket'] = $this->ticketDetails();
$form['booking'] = [
'#type' => 'fieldset',
'#title' => $this->t('Booking to transfer ticket to:'),
'booking' => [
'#type' => 'textfield',
'#title' => $this->t('Booking to transfer to'),
'#field_prefix' => $this->entity->getEvent()->get('code')->value . '-',
],
];
return $form;
}
/**
* Create the confirmation step form for transferring a ticket.
*
* @param array $form
* The current form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A Form API form for the confirmation step of the ticket transfer.
*/
protected function confirmationForm(array $form, FormStateInterface $form_state) {
$form['ticket'] = $this->ticketDetails();
$form['booking'] = $this->bookingDetails();
$paid = $this->getPaidForItem($this->entity->getOrderItem());
$transfer_payment_gateway = PaymentGateway::load('booking_transfer');
// Only show the option to transfer payments if we can calculate the amount
// paid on the Order Item and if the transfer payment gateway exists.
if ($paid && !$paid->isZero() && $transfer_payment_gateway) {
$form['transfer_payment'] = [
'#type' => 'checkbox',
'#title' => $this->t('Transfer payment with ticket'),
'#description' => $this->t('If you do not transfer existing payments then you will need to manually fix any discrepancies between order totals and payments.'),
];
}
return $form;
}
/**
* Create a render array of ticket details.
*
* @return array
* A renderable array of ticket details.
*/
protected function ticketDetails() {
$details = [
'#type' => 'fieldset',
'#title' => $this->t('Ticket to be transferred:'),
];
$details['name'] = $this->renderWithLabel($this->t('Name'), $this->entity->getName());
$details['class'] = $this->renderWithLabel($this->t('Ticket class'), $this->getClassLabel($this->entity));
if ($price = $this->entity->getPrice()) {
$details['price'] = $this->renderWithLabel($this->t('Price'), $this->currencyFormatter->format($price->getNumber(), $price->getCurrencyCode()));
}
if ($paid = $this->getPaidForItem($this->entity->getOrderItem())) {
$details['paid'] = $this->renderWithLabel($this->t('Paid'), $this->currencyFormatter->format($paid->getNumber(), $paid->getCurrencyCode()));
}
if ($balance = $this->getBalanceForItem($this->entity->getOrderItem())) {
$details['balance'] = $this->renderWithLabel($this->t('Balance'), $this->currencyFormatter->format($balance->getNumber(), $balance->getCurrencyCode()));
}
return $details;
}
/**
* Create a render array of booking details.
*
* @return array
* A renderable array of booking details.
*/
protected function bookingDetails() {
$details = [
'#type' => 'fieldset',
'#title' => $this->t('Booking to transfer ticket to:'),
];
$details['reference'] = $this->renderWithLabel($this->t('Booking reference'), $this->targetBooking->label());
$customer_link = $this->targetBooking->getCustomer()->toLink(NULL, 'canonical', ['attributes' => ['target' => '_blank']])->toString();
$details['manager'] = $this->renderWithLabel($this->t('Booking manager'), $customer_link);
return $details;
}
/**
* Get the event class label for a ticket.
*
* @param \Drupal\contacts_events\Entity\TicketInterface $ticket
* The ticket to retrieve for.
*
* @return string
* The class label.
*/
protected function getClassLabel(TicketInterface $ticket) {
if (!isset($this->classLabels)) {
$this->classLabels = array_map(function (EventClass $class) {
return $class->label();
}, $this->classStorage->loadMultiple());
}
$class = $ticket->getMappedPrice()['class'];
return $this->classLabels[$class] ?? '-';
}
/**
* Render a value with a label.
*
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $label
* The translatable label.
* @param string $value
* The value to be displayed.
*
* @return array
* A renderable array of label and value.
*/
protected function renderWithLabel(TranslatableMarkup $label, $value) {
return [
'#markup' => '<div>' . $label . ': ' . $value . '</div>',
];
}
/**
* Get a Price for the amount paid on an Order Item.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item to get the paid amount for.
*
* @return \Drupal\commerce_price\Price|null
* A Price with the paid value. NULL if not supported.
*/
protected function getPaidForItem(OrderItemInterface $order_item) {
if ($order_item->hasField('paid') && !$order_item->get('paid')->isEmpty()) {
$paid_value = $order_item->get('paid')->first()->getValue();
return new Price($paid_value['number'], $paid_value['currency_code']);
}
return NULL;
}
/**
* Get a Price for the balance of an Order Item.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item to get the balance for.
*
* @return \Drupal\commerce_price\Price|null
* A Price with the balance value. NULL if not supported.
*/
protected function getBalanceForItem(OrderItemInterface $order_item) {
if ($order_item->hasField('balance') && !$order_item->get('balance')->isEmpty()) {
$balance_value = $order_item->get('balance')->first()->getValue();
return new Price($balance_value['number'], $balance_value['currency_code']);
}
return NULL;
}
}
