commerce_alphabank_redirect-1.0.1/src/PluginForm/OffsiteRedirect/AlphabankPaymentRedirectForm.php
src/PluginForm/OffsiteRedirect/AlphabankPaymentRedirectForm.php
<?php
namespace Drupal\commerce_alphabank_redirect\PluginForm\OffsiteRedirect;
use Drupal\commerce_payment\PluginForm\PaymentOffsiteForm as BasePaymentOffsiteForm;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Provides the redirect form.
*/
class AlphabankPaymentRedirectForm extends BasePaymentOffsiteForm implements ContainerInjectionInterface {
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs a AlphabankPaymentRedirectForm object.
*
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger.
*/
public function __construct(LoggerChannelFactoryInterface $logger_factory) {
$this->logger = $logger_factory->get('commerce_alphabank_redirect');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('logger.factory')
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
/** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
$payment = $this->entity;
$payment_gateway_plugin = $payment->getPaymentGateway()?->getPlugin();
$config = $payment_gateway_plugin->getConfiguration();
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $payment->getOrder();
// Format amount.
$amount = sprintf('%0.2f', $order->getTotalPrice()?->getNumber());
// Save response data for validating response later.
$order->setData("AlphabankGatewayData", [
"shared_secret" => $config["shared_secret"],
"payment_gateway" => $payment->getPaymentGateway()?->id(),
]);
$order->save();
// Calculate digest.
$digest_data = "";
$digest_data_array = [];
$digest_data_array[1] = $config['version'];
$digest_data_array[2] = $config['mid'];
$digest_data_array[3] = "";
$digest_data_array[4] = "";
$digest_data_array[5] = $order->id() . 'at' . time();
$digest_data_array[6] = "";
$digest_data_array[7] = $amount;
$digest_data_array[8] = $config['currency'];
$digest_data_array[9] = "";
$digest_data_array[10] = "";
$digest_data_array[11] = "GR";
$digest_data_array[12] = "";
$digest_data_array[13] = $order->getBillingProfile()?->get('address')->postal_code;
$digest_data_array[14] = $order->getBillingProfile()?->get('address')->locality;
$digest_data_array[15] = $order->getBillingProfile()?->get('address')->address_line1;
$digest_data_array[16] = "";
$digest_data_array[17] = "";
$digest_data_array[18] = "";
$digest_data_array[19] = "";
$digest_data_array[20] = "";
$digest_data_array[21] = "";
$digest_data_array[22] = "";
$digest_data_array[23] = "";
$digest_data_array[24] = "";
$digest_data_array[25] = "";
$digest_data_array[26] = "";
$digest_data_array[27] = "";
$digest_data_array[28] = "";
$digest_data_array[29] = "";
$digest_data_array[30] = "";
$digest_data_array[31] = "";
$digest_data_array[32] = "";
$digest_data_array[33] = "";
$digest_data_array[34] = $config['confirmUrl'];
$digest_data_array[35] = $config['cancelUrl'];
$digest_data_array[36] = "";
$digest_data_array[37] = "";
$digest_data_array[38] = "";
$digest_data_array[39] = "";
$digest_data_array[40] = "";
$digest_data_array[41] = "";
$digest_data_array[42] = "";
$digest_data_array[43] = "";
$digest_data_array[44] = "";
$digest_data_array[45] = $config['shared_secret'];
// if IRIS payment method, add additional fields
if ($config['pay_method'] === 'iris') {
// Allow other modules to alter the billing phone field name if it differs from 'field_phone'.
$field_name = 'field_phone';
\Drupal::moduleHandler()->alter('commerce_alphabank_redirect_billing_phone_field', $field_name);
// Retrieve payer phone from the billing profile.
$payerPhone = '';
if (($billing_profile = $order->getBillingProfile()) && $billing_profile->hasField($field_name) && !$billing_profile->get($field_name)->isEmpty()) {
$payerPhone = $billing_profile->get($field_name)->value;
}
// Populate digest data array with IRIS-specific fields.
$digest_data_array[6] = $this->calculateDiasRFCode($config['beneficiary_code'], $order);
$digest_data_array[9] = $order->getEmail();
$digest_data_array[10] = $payerPhone;
$digest_data_array[26] = 'IRIS';
}
$digest_data = implode("", $digest_data_array);
$digest = base64_encode(hash("sha256", $digest_data, TRUE));
// Prepare redirect form.
$data = [
'version' => $digest_data_array[1],
'mid' => $digest_data_array[2],
'lang' => $digest_data_array[3],
'deviceCategory' => $digest_data_array[4],
'orderid' => $digest_data_array[5],
'orderDesc' => $digest_data_array[6],
'orderAmount' => $digest_data_array[7],
'currency' => $digest_data_array[8],
'payerEmail' => $digest_data_array[9],
'payerPhone' => $digest_data_array[10],
'billCountry' => $digest_data_array[11],
'billState' => $digest_data_array[12],
'billZip' => $digest_data_array[13],
'billCity' => $digest_data_array[14],
'billAddress' => $digest_data_array[15],
'weight' => $digest_data_array[16],
'dimensions' => $digest_data_array[17],
'shipCountry' => $digest_data_array[18],
'shipState' => $digest_data_array[19],
'shipZip' => $digest_data_array[20],
'shipCity' => $digest_data_array[21],
'shipAddress' => $digest_data_array[22],
'addFraudScore' => $digest_data_array[23],
'maxPayRetries' => $digest_data_array[24],
'reject3dsU' => $digest_data_array[25],
'payMethod' => $digest_data_array[26],
'trType' => $digest_data_array[27],
'extInstallmentoffset' => $digest_data_array[28],
'extInstallmentperiod' => $digest_data_array[29],
'extRecurringfrequency' => $digest_data_array[30],
'extRecurringenddate' => $digest_data_array[31],
'blockScore' => $digest_data_array[32],
'cssUrl' => $digest_data_array[33],
'confirmUrl' => $digest_data_array[34],
'cancelUrl' => $digest_data_array[35],
'var1' => $digest_data_array[36],
'var2' => $digest_data_array[37],
'var3' => $digest_data_array[38],
'var4' => $digest_data_array[39],
'var5' => $digest_data_array[40],
'var6' => $digest_data_array[41],
'var7' => $digest_data_array[42],
'var8' => $digest_data_array[43],
'var9' => $digest_data_array[44],
'digest' => $digest,
];
$form = $this->buildRedirectForm($form, $form_state, $config['postUrl'], $data, 'post');
return $form;
}
/**
* Generates a payment reference code (Dias RF code) based on the provided beneficiary code,
* order details, and calculated check digits.
*
* The function constructs the RF code using the following steps:
* - Validates the total price of the order (must be positive).
* - Calculates the "amount check digit" based on the total price.
* - Formats the debtor code (order ID) to 15 digits, padded with zeros.
* - Combines the beneficiary code, amount check digit, and debtor code
* into a temporary RF code.
* - Computes the final check digits using the Mod97 algorithm.
* - Builds the full RF code by adding the prefix "RF", the check digits,
* and the temporary RF code.
*
* @param string $beneficiary_code
* A unique string representing the beneficiary code.
* This code is used as the initial part of the RF code.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order object containing order details, including the total price.
* The order ID will be used as the debtor code, padded to 15 digits.
*
* @return string
* A 25-character string representing the complete RF payment code.
* Format: RF + 2-digit check digits + beneficiary code + amount check digit + debtor code.
*
* @throws \InvalidArgumentException
* Thrown when:
* - The total price of the order is not positive.
* - The resulting RF code does not meet the 25-character length requirement.
*
* @see calculateAmountCheckDigit()
* @see calculateCheckDigits()
*/
protected function calculateDiasRFCode($beneficiary_code, $order) {
$total_price = round($order->getTotalPrice()?->getNumber(), 2);
// Validate the total price amount
if ($total_price <= 0) {
throw new \InvalidArgumentException("The total price must be a positive amount.");
}
$amount_check_digit = $this->calculateAmountCheckDigit($total_price);
$debtor_code = str_pad($order->id(),15,"0", STR_PAD_LEFT);
$temporary_rf_code = $beneficiary_code . $amount_check_digit . $debtor_code;
$check_digits = $this->calculateCheckDigits($temporary_rf_code);
$rf_code = 'RF' . $check_digits . $temporary_rf_code;
// Ensure the code is numeric and has exactly 25 digits
if (strlen($rf_code) !== 25) {
throw new \InvalidArgumentException("RF code must a string of exactly 25 digits.");
}
return $rf_code;
}
/**
* Calculate the amount check digit (position 10) for the payment code.
*
* This function converts the given amount in euros to cents (removing the decimal point),
* processes the digits from right to left using a multiplier sequence (1, 7, 3),
* and computes the remainder when dividing the weighted sum by 8.
*
* @param float $amount The payment amount in euros.
*
* @return int The amount check digit (an integer between 0 and 7).
*
* @throws \InvalidArgumentException If the amount is negative or invalid.
*/
protected function calculateAmountCheckDigit(float $amount): int {
// Convert the amount to cents by multiplying by 100 and removing the decimal point.
$amount_in_cents = (int)round($amount * 100);
// Validate the amount in cents (ensure it's positive)
if ($amount_in_cents <= 0) {
throw new \InvalidArgumentException("Amount in cents must be a positive value.");
}
// Convert the amount to a string and reverse it to process digits from right to left.
$amount_digits = str_split(strrev((string)$amount_in_cents));
// Define the multiplier sequence.
$multipliers = [1, 7, 3];
$sum = 0;
foreach ($amount_digits as $index => $digit) {
$sum += (int)$digit * $multipliers[$index % count($multipliers)];
}
// Calculate the Mod8 remainder.
$remainder = $sum % 8;
// Return the check digit.
return $remainder;
}
/**
* Calculate the check digits (positions 3-4) for the payment code using the Mod97 algorithm.
*
* This function appends a fixed suffix (271500) to the given 21-digit temporary code,
* calculates the Mod97 remainder, and computes the check digits as `98 - remainder`.
*
* @param string $temporary_code The 21-digit temporary payment code (excluding RF and check digits).
*
* @return string The 2-digit check digits (padded with a leading zero if necessary).
*
* @throws \InvalidArgumentException If the input is not a numeric string or its length is not 21 digits.
*/
protected function calculateCheckDigits(string $temporary_code): string {
// Ensure the code is numeric and has exactly 21 digits
if (!is_numeric($temporary_code) || strlen($temporary_code) !== 21) {
throw new \InvalidArgumentException("Temporary code must be a numeric string of exactly 21 digits.");
}
// Append the fixed value 271500 to the temporary 21-digit code.
$code_with_suffix = $temporary_code . "271500";
// Calculate the Mod97 of the resulting 27-digit number.
// IMPORTANT!!! keep bcmod instead of % (for modulo operator) because $code_with_suffix is a large int.
$remainder = bcmod($code_with_suffix, 97);
// Compute the check digits.
$check_digits = 98 - $remainder;
// If the result is a single digit, prepend a 0.
return str_pad((string)$check_digits, 2, "0", STR_PAD_LEFT);
}
}
