contacts_events-8.x-1.x-dev/src/Controller/EventFinanceController.php
src/Controller/EventFinanceController.php
<?php namespace Drupal\contacts_events\Controller; use CommerceGuys\Intl\Formatter\CurrencyFormatterInterface; use Drupal\commerce_price\Price; use Drupal\contacts_events\Entity\EventInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Returns responses for Contacts Events routes. * * @todo Handle multiple currencies? */ class EventFinanceController extends ControllerBase { /** * The currency formatter. * * @var \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface */ protected $currencyFormatter; /** * The store's currency code. * * @var string|null|false */ protected $storeCurrency = FALSE; /** * The controller constructor. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface $currency_formatter * The currency formatter. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, CurrencyFormatterInterface $currency_formatter, MessengerInterface $messenger, ConfigFactoryInterface $config_factory) { $this->entityTypeManager = $entity_type_manager; $this->currencyFormatter = $currency_formatter; $this->messenger = $messenger; $this->configFactory = $config_factory; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), $container->get('commerce_price.currency_formatter'), $container->get('messenger'), $container->get('config.factory') ); } /** * Get the default currency for the store. * * @return string|null * The default currency code or NULL if there isn't one. */ protected function getCurrencyCode(): ?string { // Don't work it out more than once. if ($this->storeCurrency !== FALSE) { return $this->storeCurrency; } $this->storeCurrency = NULL; $store_id = $this->configFactory ->get('contacts_events.booking_settings') ->get('store_id'); if (!$store_id) { $this->messenger->addError($this->t('The store is not configured for bookings.')); return NULL; } /** @var \Drupal\commerce_store\Entity\StoreInterface|null $store */ $store = $this->entityTypeManager ->getStorage('commerce_store') ->load($store_id); if (!$store) { $this->messenger->addError($this->t('The store %id does not exist.', [ '%id' => $store_id, ])); return NULL; } // Get the store currency and return. return $this->storeCurrency = $store->getDefaultCurrencyCode(); } /** * Get the event specific page title. * * @param \Drupal\contacts_events\Entity\EventInterface $contacts_event * The event entity. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup * The page title. */ public function title(EventInterface $contacts_event) { return new TranslatableMarkup('Finance for @event', [ '@event' => $contacts_event->label(), ]); } /** * Build the event finance page. * * @param \Drupal\contacts_events\Entity\EventInterface $contacts_event * The event entity. * * @return array * The page render array. */ public function build(EventInterface $contacts_event) { // Ensure there is a currency code we can use. if (!$this->getCurrencyCode()) { return []; } // Ensure caches are cleared appropriately. $build['#cache']['tags'] = [ 'commerce_order_list', 'commerce_order_item_list', ]; // Get the totals for the event. $total = $this->getOrderTotal($contacts_event); if ($total === NULL) { $this->messenger->addWarning($this->t('There are no bookings for this event.')); return $build; } $build['confirmed'] = [ '#type' => 'item', '#title' => $this->t('Confirmed booking value'), '#price' => $total, '#description' => $this->t('All confirmed bookings and items.'), '#description_display' => 'after', ]; // Adjust for pending bookings. $build['pending'] = [ '#type' => 'item', '#title' => $this->t('Pending booking value'), '#price' => $this->getOrderTotal($contacts_event, ['draft']), '#description' => $this->t('Un-confirmed bookings and items.'), '#description_display' => 'after', ]; $build['total'] = [ '#type' => 'item', '#title' => $this->t('Potential booking value'), '#price' => $total, '#description' => $this->t('Including both confirmed and un-confirmed bookings and items.'), '#description_display' => 'after', ]; $build['confirmed']['#price'] = $build['total']['#price']->subtract($build['pending']['#price']); // Adjust for pending items. $adjustment = $this->getOrderItemTotal($contacts_event, ['pending']); $build['pending']['#price'] = $build['pending']['#price']->add($adjustment); $build['confirmed']['#price'] = $build['confirmed']['#price']->subtract($adjustment); // Separator before we move onto paid. $build[] = ['#markup' => '<hr/>']; // Get the total paid amount. $build['paid'] = [ '#type' => 'item', '#title' => $this->t('Fees paid to date'), '#price' => $this->getOrderTotal($contacts_event, NULL, 'total_paid'), '#description' => $this->t('Total completed payments.'), '#description_display' => 'after', ]; // Calculate the balance. $build['balance'] = [ '#type' => 'item', '#title' => $this->t('Balance due'), '#price' => $build['confirmed']['#price']->subtract($build['paid']['#price']), '#description' => $this->t('Outstanding confirmed balance.'), '#description_display' => 'after', ]; // Format the currency. foreach ($build as &$item) { if (isset($item['#price'])) { $item['#markup'] = $this->currencyFormatter->format($item['#price']->getNumber(), $item['#price']->getCurrencyCode()); } } return $build; } /** * Get a total amount for orders. * * @param \Drupal\contacts_events\Entity\EventInterface $event * The event. * @param array|null $state * Optionally filter by state. * @param string $field * The field to aggregate on, either 'total_price' or 'total_paid'. * * @return \Drupal\commerce_price\Price * A price item, of zero value if there are no results. */ protected function getOrderTotal(EventInterface $event, array $state = NULL, string $field = 'total_price'): Price { if (!$this->getCurrencyCode()) { throw new \Exception('Missing currency code.'); } // Build our query. $query = $this->entityTypeManager ->getStorage('commerce_order') ->getAggregateQuery(); $query->accessCheck(FALSE); $query->addTag('contacts_events_finance'); $query->addMetaData('contacts_events_finance', compact('event', 'state', 'field')); $query->condition('type', 'contacts_booking'); $query->condition('event', $event->id()); $query->condition($field . '.currency_code', $this->getCurrencyCode()); // Sum the field number and group on the field currency code. $query->aggregate($field . '.number', 'SUM'); // Add the optional state filter. if ($state) { $query->condition('state', $state, 'IN'); } // Extract our result. $result = $query->execute(); return new Price($result[0][$field . 'number_sum'] ?? '0', $this->getCurrencyCode()); } /** * Get a total amount for orders. * * @param \Drupal\contacts_events\Entity\EventInterface $event * The event. * @param array|null $state * Optionally filter by state. * * @return \Drupal\commerce_price\Price|null * A price item, or NULL if there are no results and no default currency. */ protected function getOrderItemTotal(EventInterface $event, array $state = NULL) { if (!$this->getCurrencyCode()) { throw new \Exception('Missing currency code.'); } // Build our query. $query = $this->entityTypeManager ->getStorage('commerce_order_item') ->getAggregateQuery(); $query->accessCheck(FALSE); $query->addTag('contacts_events_finance'); $query->addMetaData('contacts_events_finance', compact('event', 'state')); $query->condition('order_id.entity.type', 'contacts_booking'); $query->condition('order_id.entity.event', $event->id()); $query->condition('order_id.entity.state', 'draft', '!='); $query->condition('state', $state); $query->condition('total_price.currency_code', $this->getCurrencyCode()); // Sum the total price number and group on the total price currency code. $query->aggregate('total_price.number', 'SUM'); // Add the optional state filter. if ($state) { $query->condition('state', $state, 'IN'); } // Extract our result. $result = $query->execute(); return new Price($result[0]['total_pricenumber_sum'] ?? '0', $this->getCurrencyCode()); } }