commerce_funds-8.x-1.7/src/Element/FundsTransaction.php
src/Element/FundsTransaction.php
<?php
namespace Drupal\commerce_funds\Element;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElementBase;
use Drupal\Core\Url;
use Drupal\commerce_funds\Entity\Transaction;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
/**
* Provides a transaction form element.
*
* Usage example:
* @code
* $form['transaction'] = [
* '#type' => 'funds_transaction',
* '#available_currencies' => ['USD', 'EUR'],
* '#notes_enabled' => TRUE,
* '#transaction_type' => 'transfer',
* ];
* @endcode
*
* @FormElement("funds_transaction")
*/
class FundsTransaction extends FormElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
// List of currencies. If empty, all currencies will be available.
'#available_currencies' => [],
'#notes_enabled' => TRUE,
'#transaction_type' => NULL,
'#process' => [
[$class, 'processFundsTransaction'],
],
'#element_validate' => [
[$class, 'moveInlineErrors'],
],
];
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
$currency_input = $element['#transaction_type'] === 'conversion' ? 'currency_left' : 'currency';
$op = $form_state->getUserInput()['op'] ?? NULL;
if ($input && $input[$currency_input] && $op == 'Save') {
// We need to validate before the value is actually set.
$validated = self::validateTransaction($element, $input, $form_state);
if ($validated) {
// Prepares variables for later.
$amount = $input['amount'];
$currency = $input[$currency_input];
$issuer_uid = \Drupal::currentUser()->id();
$transaction_type = $element['#transaction_type'];
// Calculates fees applied.
if ($transaction_type === 'conversion') {
$conversion = \Drupal::service('commerce_funds.fees_manager')->convertCurrencyAmount($amount, $currency, $input['currency_right']);
$fee_applied = [
'net_amount' => $conversion['new_amount'],
'fee' => $conversion['rate'],
];
}
else {
$method = $transaction_type === 'withdrawal_request' ? 'withdraw_' . $input['methods'] : $transaction_type;
$fee_applied = \Drupal::service('commerce_funds.fees_manager')->calculateTransactionFee($amount, $currency, $method);
}
// Transfer and escrow.
$recipient_uid = isset($input['username']) ? EntityAutocomplete::extractEntityIdFromAutocompleteInput($input['username']) : FALSE;
// Deposit, Withdrawal request and conversion.
if (!$recipient_uid) {
$recipient_uid = \Drupal::currentUser()->id();
}
// Creates transaction.
// We set it as canceled to validate it later.
$transaction = Transaction::create([
'issuer' => $issuer_uid,
'recipient' => $recipient_uid,
'type' => $transaction_type,
'method' => $transaction_type == 'withdrawal_request' ? $input['methods'] : 'internal',
'brut_amount' => $amount,
'net_amount' => $fee_applied['net_amount'],
'fee' => $fee_applied['fee'],
'currency' => $currency_input == 'currency' ? $currency : $input['currency_right'],
'status' => Transaction::TRANSACTION_STATUS['canceled'],
'notes' => [
'value' => $input['notes']['value'] ?? '',
'format' => $input['notes']['format'] ?? '',
],
]);
// Conversion ? add currency right and notes.
if ($currency_input == 'currency_left') {
$transaction->setFromCurrencyCode($input['currency_left']);
$transaction->setNotes([
'value' => t('@amount @currency_left converted into @new_amount @currency_right.', [
'@amount' => $amount,
'@currency_left' => $input['currency_left'],
'@new_amount' => $fee_applied['net_amount'],
'@currency_right' => $input['currency_right'],
]),
'format' => 'basic_html',
]);
}
$transaction->save();
$element['#processed'] = TRUE;
}
}
return isset($transaction) ? $transaction->id() : NULL;
}
/**
* Processes the funds_transaction form element.
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processFundsTransaction(array &$element, FormStateInterface $form_state, array &$complete_form) {
/** @var \Drupal\commerce_funds\FeesManagerInterface $fees_manager */
$fees_manager = \Drupal::service('commerce_funds.fees_manager');
$element['#tree'] = TRUE;
// Default widget - No store.
if (!\Drupal::entityTypeManager()->getStorage('commerce_store')->loadDefault()) {
\Drupal::messenger()->addError(t('You haven\'t configured a store yet. Please <a href=":url">configure one</a> before.', [
':url' => Url::fromRoute('entity.commerce_store.collection')->toString(),
]));
return FALSE;
}
// Default widget - Set transaction type if changed.
if (isset($form_state->getValue('settings')['transaction_type'])) {
$element['#transaction_type'] = $form_state->getValue('settings')['transaction_type'];
}
// Default widget - Set selected currency if defined.
if (isset($form_state->getValue('settings')['available_currencies'])) {
$element['#available_currencies'] = $form_state->getValue('settings')['available_currencies'];
}
// Default widget - Set notes enabled.
if (isset($form_state->getValue('settings')['enable_notes'])) {
$element['#notes_enabled'] = $form_state->getValue('settings')['enable_notes'];
}
// Build default currency list.
$currencies = \Drupal::entityTypeManager()->getStorage('commerce_currency')->loadMultiple();
$currency_codes = [];
/** @var \Drupal\commerce_price\Entity\Currency $currency */
foreach ($currencies as $currency) {
$currency_codes[$currency->getCurrencyCode()] = $currency->getCurrencyCode();
}
// Make sure currencies are sorted.
ksort($currency_codes);
// If currencies are filters, only display filtered ones.
if (!empty($element['#available_currencies'])) {
$currency_codes = array_intersect($currency_codes, $element['#available_currencies']);
}
if ($transaction_type = $element['#transaction_type']) {
$fees_description = $fees_manager->printTransactionFees($transaction_type);
}
if ($transaction_type == 'conversion') {
$element['currency_left'] = [
'#type' => 'select',
'#title' => t('From'),
'#description' => t('The currency to convert.'),
'#options' => $currency_codes,
'#ajax' => [
'callback' => [
'Drupal\commerce_funds\Element\FundsTransaction',
'printRate',
],
'event' => 'change',
'wrapper' => 'exchange-rate',
'progress' => [
'type' => 'throbber',
'message' => t('Calculating rate...'),
],
],
];
}
$transaction_name = ucfirst(str_replace('_', ' ', $transaction_type));
$element['amount'] = [
'#type' => 'number',
'#min' => 0.0,
'#title' => t('@transaction_type amount', [
'@transaction_type' => $transaction_name ,
]),
'#description' => $fees_description ?? t('Fees applied will appear here.'),
'#step' => 0.01,
'#size' => 30,
'#maxlength' => 128,
'#required' => TRUE,
'#attributes' => [
'class' => ['funds-amount'],
],
];
if (isset($fees_description) && \Drupal::config('commerce_funds.settings')->get('global')['add_rt_fee_calculation']) {
$element['amount'] += [
'#attached' => [
'library' => ['commerce_funds/calculate_fees'],
'drupalSettings' => [
'funds' => ['fees' => $fees_description],
],
],
];
}
if ($transaction_type == 'conversion') {
$element['amount'] = array_merge($element['amount'], [
'#ajax' => [
'callback' => [
'Drupal\commerce_funds\Element\FundsTransaction',
'printRate',
],
'event' => 'end_typing',
'wrapper' => 'exchange-rate',
'progress' => [
'type' => 'throbber',
'message' => t('Calculating rate...'),
],
],
'#attributes' => [
'class' => ['delayed-input-submit'],
],
'#attached' => [
'library' => ['commerce_funds/delayed_submit'],
],
]);
}
if ($transaction_type == 'conversion') {
$element['currency_right'] = [
'#type' => 'select',
'#title' => t('To'),
'#description' => t('The to currency to convert into.'),
'#options' => $currency_codes,
'#ajax' => [
'callback' => [
'Drupal\commerce_funds\Element\FundsTransaction',
'printRate',
],
'event' => 'change',
'wrapper' => 'exchange-rate',
'progress' => [
'type' => 'throbber',
'message' => t('Calculating rate...'),
],
],
];
$element['ajax_container'] = [
'#type' => 'container',
'#attributes' => ['id' => 'exchange-rate'],
];
}
if ($transaction_type != 'conversion') {
$element['currency'] = [
'#type' => 'select',
'#title' => t('Select Currency'),
'#options' => $currency_codes,
];
}
if (in_array($transaction_type, ['transfer', 'escrow'])) {
$element['username'] = [
'#id' => 'commerce-funds-transaction-to',
'#title' => t('@transaction_type to', [
'@transaction_type' => $transaction_name,
]),
'#description' => t('Please enter a username.'),
'#type' => 'entity_autocomplete',
'#target_type' => 'user',
'#required' => TRUE,
'#size' => 30,
'#maxlength' => 128,
'#selection_settings' => [
'include_anonymous' => FALSE,
],
];
}
if ($transaction_type == 'withdrawal_request') {
$methods = array_filter(\Drupal::config('commerce_funds.settings')->get('withdrawal_methods'));
foreach ($methods as $key => $method) {
$fee = $fees_manager->printPaymentGatewayFees($key, t('unit(s)'), 'withdraw') ?: '';
$enabled_method['methods'][$key] = ucfirst($method) . ' ' . $fee;
}
if (empty($enabled_method['methods'])) {
$enabled_method['methods'] = [];
\Drupal::messenger()->addWarning(t('No withdrawal method available.'));
}
$element['methods'] = [
'#type' => 'radios',
'#options' => str_replace('_', ' ', $enabled_method['methods']),
'#title' => t('Select your preferred withdrawal method.'),
'#required' => TRUE,
];
}
$transaction_types = ['transfer', 'escrow'];
if ($element['#notes_enabled'] && in_array($transaction_type, $transaction_types)) {
$element['notes'] = [
'#type' => 'text_format',
'#title' => t('Notes'),
'#description' => t('Eventually add a message to the recipient.'),
];
}
// Check permissions on element.
$permissions = [
'deposit' => 'deposit funds',
'escrow' => 'create escrow payment',
'transfer' => 'transfer funds',
'withdrawal_request' => 'withdraw funds',
'conversion' => 'convert currencies',
];
// If transaction type unknown or undefined,
// give access to the field (new bundle or default widget).
$element['#access'] = isset($permissions[$transaction_type]) ? \Drupal::currentUser()->hasPermission($permissions[$transaction_type]) : TRUE;
return $element;
}
/**
* Element validation handler.
*
* @param array $element
* The form element.
* @param array $input
* The inputs inserted by the user.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* Validated or not.
*/
public static function validateTransaction(array $element, array $input, FormStateInterface $form_state) {
$validated = TRUE;
$transaction_type = $element['#transaction_type'];
/** @var \Drupal\commerce_funds\FeesManagerInterface $fees_manager */
$fees_manager = \Drupal::service('commerce_funds.fees_manager');
// No validation for deposit.
if ($transaction_type == 'deposit') {
return $validated;
}
// Load variables for later.
$amount = $input['amount'];
$currency = $transaction_type == 'conversion' ? $input['currency_left'] : $input['currency'];
$fee_applied = $fees_manager->calculateTransactionFee($amount, $currency, $transaction_type);
$issuer = \Drupal::currentUser();
$issuer_balance = \Drupal::service('commerce_funds.transaction_manager')->loadAccountBalance($issuer->getAccount(), $currency);
$currency_balance = $issuer_balance[$currency] ?? 0;
// Error if amount equals 0.
if ($amount == 0) {
$form_state->setErrorByName('amount', t('Amount must be a positive number.'));
$validated = FALSE;
return $validated;
}
// Error if the user doesn't have enough money
// to cover the transaction + fee.
if ($currency_balance < $fee_applied['net_amount']) {
if (!$fee_applied['fee']) {
$form_state->setErrorByName('amount', t("Not enough funds to cover this transaction."));
$validated = FALSE;
}
if ($fee_applied['fee']) {
$form_state->setErrorByName('amount', t("Not enough funds to cover this transaction (Total: %total @currency).", [
'%total' => $fee_applied['net_amount'],
'@currency' => $currency,
]));
$validated = FALSE;
}
}
if (isset($input['methods']) && ($method = $input['methods'])) {
$issuer_data = \Drupal::service('user.data')->get('commerce_funds', $issuer->id(), $method);
if (!$issuer_data) {
$form_state->setErrorByName('methods', t('Please <a href="@enter_details_link">enter your details</a> for this withdrawal method first.', [
'@enter_details_link' => Url::fromRoute('commerce_funds.withdrawal_methods.edit', [
'user' => $issuer->id(),
'method' => str_replace('_', '-', $method),
], [
'query' => [
'destination' => \Drupal::request()->getRequestUri(),
],
])->toString(),
]));
$validated = FALSE;
}
}
if (in_array($transaction_type, ['transfer', 'escrow'])) {
$match = EntityAutocomplete::extractEntityIdFromAutocompleteInput($input['username']);
// Error if user try to send money to itself.
if ($match && $issuer->id() == $match) {
$form_state->setErrorByName('username', t("Operation impossible. You can't transfer money to yourself."));
$validated = FALSE;
}
// Error recipient is not matching any user.
// As our validation happens during the value callback,
// we need to make our own checks and stop the process
// before the transaction is created. The field reference then handle
// the error message.
if ($match) {
if (!User::load($match)) {
$validated = FALSE;
}
}
if (!$match) {
$validated = FALSE;
}
}
// Conversion: amount after conversion equals 0.
if ($transaction_type === 'conversion') {
// You can't convert a currency into itself.
if ($currency === $input['currency_right']) {
$form_state->setErrorByName('currency_right', t('Operation impossible. Please chose another currency.'));
$validated = FALSE;
}
else {
// No exchange rates.
if (!$fees_manager->getExchangeRates()) {
$form_state->setErrorByName('currency_right', t("Operation impossible. No exchange rates found."));
$validated = FALSE;
}
else {
// If amount after conversion equals 0.
$conversion = $fees_manager->convertCurrencyAmount($amount, $currency, $input['currency_right']);
if (!(float) $conversion['new_amount']) {
$form_state->setErrorByName('currency_right', t('Operation impossible. No exchange rates found.'));
$validated = FALSE;
}
}
}
}
return $validated;
}
/**
* Entity builder handler.
*
* @see Drupal\commerce_funds\Plugin\Field\FieldWidget\FundsTransactionTransferWidget::formElement()
*/
public static function updateFundsTransaction($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) {
$op = $form_state->getUserInput()['op'] ?? NULL;
// Only run this on submission with no errors
// And on form save.
if ($form_state->isSubmitted() && !$form_state->hasAnyErrors() && $op == 'Save') {
// Find the funds_transaction fields.
$transaction_ids = [];
foreach (Element::children($form) as $field_name) {
if (isset($form[$field_name]['widget'][0]['target_id']['#type']) && $form[$field_name]['widget'][0]['target_id']['#type'] == 'funds_transaction') {
$transaction_ids[] = $form_state->getValue($field_name)[0]['target_id'];
}
}
foreach ($transaction_ids as $transaction_id) {
if (!is_array($transaction_id) && !empty($transaction_id)) {
$transaction = Transaction::load($transaction_id);
// Make sure we never trigger a transaction
// already completed or pending.
$transaction_status = [
Transaction::TRANSACTION_STATUS['completed'],
Transaction::TRANSACTION_STATUS['pending'],
];
if (!in_array($transaction->getStatus(), $transaction_status)) {
$transaction_manager = \Drupal::service('commerce_funds.transaction_manager');
$transaction_type = $transaction->bundle();
// Deposit and withdrawal don't trigger
// perform transaction on submission.
if (!in_array($transaction_type, ['deposit', 'withdrawal_request'])) {
// Performs transaction.
$transaction_manager->performTransaction($transaction);
// Send emails.
if ($transaction_type !== 'conversion') {
$transaction_manager->sendTransactionMails($transaction);
}
// Generate confirmation message.
$transaction_manager->generateConfirmationMessage($transaction);
}
elseif ($transaction_type === 'withdrawal_request') {
// Set status to pending.
$transaction->setStatus(Transaction::TRANSACTION_STATUS['pending']);
$transaction->save();
// Generate confirmation message.
$transaction_manager->generateConfirmationMessage($transaction);
}
}
}
}
}
}
/**
* Moves inline errors from the global element sub elements.
*
* This ensures that they are displayed in the right place.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function moveInlineErrors(array $element, FormStateInterface $form_state) {
$errors = $form_state->getErrors();
foreach ($errors as $element_name => $error) {
// In case error is generated by the field
// and not our validation handler we clean it.
$parts = explode('[', $element_name);
$element_name = array_pop($parts);
// Reset error on right element.
$form_state->setError($element[$element_name], $error);
}
}
/**
* Ajax callback.
*/
public static function printRate($form, FormStateInterface $form_state) {
$fees_manager = \Drupal::service('commerce_funds.fees_manager');
$exchange_rates = $fees_manager->getExchangeRates();
$triggering_element = $form_state->getTriggeringElement();
$field_name = reset($triggering_element['#array_parents']);
$rate_description = '';
if (!empty($form_state->getUserInput()[$field_name])) {
$inputs = $form_state->getUserInput()[$field_name][0]['target_id'];
if ($inputs['currency_left'] !== $inputs['currency_right']) {
$new_amount = $fees_manager->printConvertedAmount($inputs['amount'], $inputs['currency_left'], $inputs['currency_right']);
$rate_description = t('Conversion rate applied: @exchange-rate <br> Amount after conversion: @new_amount', [
'@exchange-rate' => $exchange_rates ? $exchange_rates[$inputs['currency_left']][$inputs['currency_right']]['value'] : 0,
'@new_amount' => $new_amount,
]);
}
}
$form[$field_name]['widget'][0]['target_id']['ajax_container']['markup'] = [
'#markup' => $rate_description ?: t('Conversion rate applied: 1'),
'#attributes' => [
'id' => ['rate-output'],
],
];
return $form[$field_name]['widget'][0]['target_id']['ajax_container'];
}
}
