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());
}
}
