mutual_credit-5.0.x-dev/src/Entity/Transaction.php
src/Entity/Transaction.php
<?php namespace Drupal\mcapi\Entity; use Drupal\mcapi\Entity\Currency; use Drupal\mcapi\Entity\Type; use Drupal\mcapi\Entity\State; use Drupal\mcapi\Event\TransactionAssembleEvent; use Drupal\mcapi\Event\McapiEvents; use Drupal\user\UserInterface; use Drupal\user\EntityOwnerTrait; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Messenger\MessengerTrait; /** * Defines the Transaction entity. * * Transactions are grouped and handled by serial number but the unique database * key is the xid. * * @ContentEntityType( * id = "mcapi_transaction", * label = @Translation("Transaction"), * label_collection = @Translation("Transactions"), * handlers = { * "storage" = "Drupal\mcapi\Entity\Storage\TransactionStorage", * "storage_schema" = "Drupal\mcapi\Entity\Storage\TransactionStorageSchema", * "view_builder" = "Drupal\mcapi\Entity\ViewBuilder\TransactionViewBuilder", * "list_builder" = "\Drupal\mcapi\Entity\ListBuilder\TransactionListBuilder", * "access" = "Drupal\mcapi\Entity\Access\TransactionAccessControlHandler", * "form" = { * "default" = "Drupal\mcapi\Form\TransactionForm", * "bill" = "Drupal\mcapi\Form\TransactionForm", * "credit" = "Drupal\mcapi\Form\TransactionForm" * }, * "views_data" = "Drupal\mcapi\Entity\Views\TransactionViewsData", * "route_provider" = { * "html" = "Drupal\mcapi\Entity\RouteProvider\TransactionRouteProvider", * }, * }, * admin_permission = "configure mcapi", * base_table = "mcapi_transaction", * entity_keys = { * "id" = "xid", * "uuid" = "uuid", * "name" = "description", * "owner" = "creator" * }, * field_ui_base_route = "entity.mcapi_transaction.collection", * translatable = FALSE, * links = { * "canonical" = "/transaction/{mcapi_transaction}", * "add-form" = "/transact/add", * "collection" = "/admin/accounting/transactions" * }, * constraints = { * "DifferentWallets" = {} * } * ) */ class Transaction extends ContentEntityBase implements TransactionInterface, \IteratorAggregate { use MessengerTrait; use EntityChangedTrait; use EntityOwnerTrait; protected $eventDispatcher; public bool $assembled = FALSE; public bool $isChild = FALSE; /** * Constructor. * * @note There seems to be no way to inject anything here. * * @see SqlContentEntityStorage::mapFromStorageRecords */ public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { parent::__construct($values, $entity_type, $bundle, $translations); $this->eventDispatcher = \Drupal::service('event_dispatcher'); } /** * {@inheritDoc} */ public function label($langcode = NULL) { return t("Transaction #@serial", ['@serial' => $this->serial->value]); } /** * {@inheritDoc} * * @note you cannot load a transaction by its xid. */ public static function load($serial) { return static::loadBySerial($serial); } /** * {@inheritDoc} */ public static function loadBySerial(int $serial) : Transaction { $storage = \Drupal::entityTypeManager()->getStorage('mcapi_transaction'); $xid = $storage->getParentXid($serial); $results = $storage->loadByProperties(['xid' => $xid]); // Should be only one result [$xid => $serial] return reset($results); } /** * {@inheritDoc} */ protected function urlRouteParameters($rel) { return [ 'mcapi_transaction' => $this->serial->value, ]; } /** * {@inheritDoc} */ public function assemble() : array { $new_children = []; if (!$this->assembled and $this->isNew() and !$this->isChild) { $start = count($this->children); $this->eventDispatcher->dispatch( new TransactionAssembleEvent($this), McapiEvents::ASSEMBLE ); $new_children = array_slice($this->children->referencedEntities(), $start); } $this->assembled = TRUE; return $new_children; } /** * Validate a transaction (including children). */ public function validate() : EntityConstraintViolationListInterface { static $parent = TRUE; if ($parent and !$this->assembled) { $this->assemble(); } $parent = FALSE; // prevents infinite recursion. /* @var EntityConstraintViolationListInterface $violationList */ $violationList = parent::validate(); // The child referenced entities aren't validated automatically. foreach ($this->children->referencedEntities() as $child) { foreach ($child->validate() as $violation) { $violationList->add($violation); } } return $violationList; } /** * {@inheritDoc} */ public static function preCreate(EntityStorageInterface $storage, array &$values) { $type_name = isset($values['type']) ? $values['type'] : 'default'; if ($type = Type::load($type_name)) { // doesn't apply to imported transactions or child transactions. if (empty($values['serial']) and empty($values['state'])) { $values['state'] = 'temp'; } } else { trigger_error("Replaced bad transaction type '$type_name' with 'default'", E_USER_ERROR); } if (empty($values['worth']) and isset($values['quantity'])) { if ($curr = $values['currency'] ?? $values['currency_id'] ?? $values['curr_id']) { $values['worth'][] = [ 'curr_id' => $curr, 'value' => $values['quantity'] ]; unset($values['currency'], $values['currcode'], $values['curr_id']); } else { throw new \Exception('Cannot derive worth field from given fields: '.print_r($values, 1)); } } $values += [ 'type' => 'default', // Uid of 0 means drush must have created it. 'creator' => static::getDefaultEntityOwner(), 'state' => 'done' ]; // There isn't a hook these can be moved into. if (isset($values['payer']) and is_array($values['payer'])) { $values['type'] = 'mass'; $payers = $values['payer']; $values['payer']= array_shift($payers); foreach($payers as $payer) { $values['children'][] = static::create(['payer' => $payer] + $values); } } elseif (isset($values['payee']) and is_array($values['payee'])) { $values['type'] = 'mass'; $payees = $values['payee']; $values['payee'] = array_shift($payees); foreach($payees as $payee) { $values['children'][] = static::create(['payee' => $payee] + $values); } } } /** * {@inheritDoc} */ public function getCacheTagsToInvalidate() { $tags = parent::getCacheTagsToInvalidate(); // Invalidate tags of wallets in child transactions. foreach ($this->flatten() as $transaction) { foreach (['payer', 'payee'] as $actor) { if ($wallet = $transaction->{$actor}->entity) { $tags = array_merge_recursive($tags, $wallet->getCacheTagsToInvalidate()); } } } // We should only be clearing the currency displays, but I haven't worked out the cache tags for that. foreach (Currency::loadMultiple($this->worth->currencies()) as $currency) { $tags = array_merge($tags, $currency->getCacheTagsToInvalidate()); } return $tags; } /** * {@inheritDoc} */ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields = parent::baseFieldDefinitions($entity_type); $fields += static::ownerBaseFieldDefinitions($entity_type); $fields['description'] = BaseFieldDefinition::create('string') ->setLabel(t('Description')) ->setDescription(t('The good or service.')) ->setSettings(['max_length' => 255]) ->setRequired(FALSE) ->setDefaultValue('') ->setDisplayConfigurable('form', TRUE) ->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 3]) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view', ['type' => 'string', 'weight' => 3]); $fields['serial'] = BaseFieldDefinition::create('integer') ->setLabel(t('Serial number')) ->setDescription(t('Grouping of related transactions.')) ->setReadOnly(TRUE) ->setRequired(FALSE); $fields['worth'] = BaseFieldDefinition::create('worth') ->setLabel(t('Worth')) ->setDescription(t('Value of the transaction (one or more currencies)')) ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->setRequired(TRUE) ->addPropertyConstraints('curr_id', [ 'AllowedValues' => ['callback' => ['Drupal\mcapi\Entity\Transaction', 'getAllCurrencyIds']], ]) ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view', ['weight' => 4]); $fields[PAYER_FIELDNAME] = BaseFieldDefinition::create('wallet_reference') ->setLabel(t('Payer')) ->setDescription(t('The giving wallet')) ->setReadOnly(TRUE) ->setRequired(TRUE) ->setCardinality(1) ->setDisplayConfigurable('form', TRUE) ->setDisplayOptions('form', ['type' => 'wallet_reference_autocomplete', 'weight' => 1]) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view', ['type' => 'entity_reference_label', 'weight' => 1]); $fields[PAYEE_FIELDNAME] = BaseFieldDefinition::create('wallet_reference') ->setLabel(t('Payee')) ->setDescription(t('The receiving wallet')) ->setReadOnly(TRUE) ->setRequired(TRUE) ->setCardinality(1) ->setDisplayConfigurable('form', TRUE) ->setDisplayOptions('form', ['type' => 'wallet_reference_autocomplete', 'weight' => 2]) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view', ['type' => 'entity_reference_label', 'weight' => 2]); $fields['creator'] // Alterations to ownerBaseFieldDefinition ->setLabel(t('Creator')) ->setDescription(t('The user who created the transaction')) ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions( 'form', ['type' => 'hidden', 'weight' => 10] ) ->setReadOnly(TRUE) ->setRequired(TRUE) ->setRevisionable(FALSE); $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Created on')) ->setDescription(t('The time the transaction was created.')) ->setRevisionable(FALSE) ->setRequired(TRUE) ->setDisplayOptions( 'form', ['type' => 'hidden', 'weight' => 10] ) ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) ->setDescription(t('The time that the transaction was last saved.')) ->setDisplayConfigurable('view', TRUE) ->setInitialValue('created') ->setRevisionable(FALSE) ->setRequired(TRUE) ->setTranslatable(FALSE); $fields['type'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Type')) ->setDescription(t('The type/workflow path of the transaction')) ->setSetting('target_type', 'mcapi_type') ->setDisplayConfigurable('view', TRUE) ->setDisplayConfigurable('form', TRUE) ->setDisplayOptions('form', ['region' => 'hidden']) ->addPropertyConstraints('target_id', [ 'AllowedValues' => ['callback' => ['Drupal\mcapi\Entity\Transaction', 'getAllowedTypes']], ]) ->setReadOnly(TRUE) ->setRequired(TRUE); $fields['state'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('State')) ->setDescription(t('Completed, pending, disputed, etc.')) ->setSetting('target_type', 'mcapi_state') ->setDefaultValue('temp') ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('form', ['region' => 'hidden']) ->addPropertyConstraints('target_id', [ 'AllowedValues' => ['callback' => ['Drupal\mcapi\Entity\Transaction', 'getAllowedStates']], ]) ->setReadOnly(FALSE) ->setRequired(TRUE); $fields['children'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Children')) ->setDescription(t('Dependent transactions such as fees')) ->setSetting('target_type', 'mcapi_transaction') ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->setRevisionable(FALSE) ->setDisplayConfigurable('view', true) ->setDisplayConfigurable('form', false); return $fields; } /** * {@inheritDoc} */ public function getOwner() { return $this->creator->entity; } /** * {@inheritDoc} */ public function getOwnerId() { return $this->creator->target_id; } /** * {@inheritDoc} */ public function setOwner(UserInterface $account) { $this->creator = $account; } /** * {@inheritDoc} */ public function setOwnerId($uid) { $this->creator->targetId = $uid; } /** * {@inheritDoc} */ public function getCreatedTime() { return $this->get('created')->value; } /** * {@inheritDoc} */ public function toArray() { $array = parent::toArray(); foreach ($this->children->referencedEntities() as $key => $transaction) { $array['children'][$key]['entity'] = $transaction->toArray(); } return $array; } /** * Options callback for Transaction::state field. */ static function getAllowedStates() :array { return array_keys(State::LoadMultiple()); } /** * Options callback for Transaction::type field. */ static function getAllowedTypes() { return array_keys(Type::LoadMultiple()); } /** * Options callback for Transaction::worth::curr_id field. */ static function getAllCurrencyIds() { return array_keys(Currency::LoadMultiple()); } /** * {@inheritDoc} * @deprecated but removing requires reinstalling the field definition and content as in mcapi_update_8998 */ static function getAllowedCurrencies() { return static::getAllCurrencyIds(); } /** * {@inheritDoc} * * Workaround for when the content_translation module tries to link to the * original language before the entity is saved and an id is available on transaction/0/save * I don't know why the entity definition 'translatable = FALSE' doesn't apply */ public function hasLinkTemplate($rel) { if (!$this->isNew()) { return parent::hasLinkTemplate($rel); } return FALSE; } /** * Make a child as a clone of the $this, and modify given fields. * @param array $params * @throws \Exception */ public function createChild(array $params) : static { $logger = \Drupal::logger('mcapi'); if ($params['payer'] == $params['payee']) { throw new \Exception('Skipped child transaction with same payer and payee: '.$params['payee']); } $params['worth']['curr_id'] = $params['curr_id'] ?? $this->worth->curr_id; unset($params['curr_id']); if (isset($params['quantity'])) { $params['worth']['value'] = $params['quantity']; unset($params['quantity']); } unset($params['state']); $params += ['type' => 'inherit']; $params += $this->toArray(); // This prevents validation recursing through the children and will be overwritten $child = Transaction::Create(); foreach ($params as $field => $val) { if (in_array($field, ['xid', 'uuid', 'children'])) continue; $child->set($field, $val); } // Check the value wasn't rounded down to nothing if ($child->worth->isEmpty()) { throw new \Exception('Empty worth value in child transaction'); } $child->isChild = TRUE; $this->children->appendItem($child); return $child; } /** * {@inheritDoc} */ function onChange($name) { parent::onChange($name); if ($name == 'state' or $name == 'serial') { foreach ($this->children->referencedEntities() as $child) { $child->{$name}->setValue($this->{$name}->getValue()); } } } /** * Iterate through the transaction and children as if it was an array. */ public function flatten() { yield $this; foreach ($this->children->referencedEntities() as $child) { yield $child; } } /** * Delete all the transaction children. */ public function delete() { foreach ($this->children->referencedEntities() as $t) { $t->delete(); } parent::delete(); } }