commerce_paypal-8.x-1.0-beta11/src/Plugin/Commerce/PaymentGateway/Payflow.php
src/Plugin/Commerce/PaymentGateway/Payflow.php
<?php
namespace Drupal\commerce_paypal\Plugin\Commerce\PaymentGateway;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce_payment\CreditCard;
use Drupal\commerce_payment\Entity\PaymentInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Exception\HardDeclineException;
use Drupal\commerce_payment\Exception\InvalidRequestException;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayBase;
use Drupal\commerce_paypal\Event\PayPalEvents;
use Drupal\commerce_paypal\Event\PayflowRequestEvent;
use Drupal\commerce_price\Price;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the PayPal Payflow payment gateway.
*
* @CommercePaymentGateway(
* id = "paypal_payflow",
* label = "PayPal - Payflow (Legacy)",
* display_label = "Credit Card",
* payment_method_types = {"credit_card"},
* credit_card_types = {
* "amex", "dinersclub", "discover", "jcb", "maestro", "mastercard", "visa", "unionpay"
* },
* )
*/
class Payflow extends OnsitePaymentGatewayBase implements PayflowInterface {
/**
* Payflow test API URL.
*/
public const PAYPAL_API_TEST_URL = 'https://pilot-payflowpro.paypal.com';
/**
* Payflow production API URL.
*/
public const PAYPAL_API_URL = 'https://payflowpro.paypal.com';
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The rounder.
*
* @var \Drupal\commerce_price\RounderInterface
*/
protected $rounder;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->httpClient = $container->get('http_client');
$instance->rounder = $container->get('commerce_price.rounder');
$instance->eventDispatcher = $container->get('event_dispatcher');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'partner' => '',
'vendor' => '',
'user' => '',
'password' => '',
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['partner'] = [
'#type' => 'textfield',
'#title' => $this->t('Partner'),
'#default_value' => $this->configuration['partner'],
'#required' => TRUE,
];
$form['vendor'] = [
'#type' => 'textfield',
'#title' => $this->t('Vendor'),
'#default_value' => $this->configuration['vendor'],
'#required' => TRUE,
];
$form['user'] = [
'#type' => 'textfield',
'#title' => $this->t('User'),
'#default_value' => $this->configuration['user'],
'#required' => TRUE,
];
$form['password'] = [
'#type' => 'password',
'#title' => $this->t('Password'),
'#description' => $this->t('Only needed if you wish to change the stored value.'),
'#default_value' => $this->configuration['password'],
'#required' => empty($this->configuration['password']),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
if (!$form_state->getErrors()) {
$values = $form_state->getValue($form['#parents']);
$this->configuration['partner'] = $values['partner'];
$this->configuration['vendor'] = $values['vendor'];
$this->configuration['user'] = $values['user'];
if (!empty($values['password'])) {
$this->configuration['password'] = $values['password'];
}
}
}
/**
* Returns the Api URL.
*/
protected function getApiUrl() {
return $this->getMode() === 'test' ? self::PAYPAL_API_TEST_URL : self::PAYPAL_API_URL;
}
/**
* Returns the partner.
*/
protected function getPartner() {
return $this->configuration['partner'] ?: '';
}
/**
* Returns the vendor.
*/
protected function getVendor() {
return $this->configuration['vendor'] ?: '';
}
/**
* Returns the user.
*/
protected function getUser() {
return $this->configuration['user'] ?: '';
}
/**
* Returns the password.
*/
protected function getPassword() {
return $this->configuration['password'] ?: '';
}
/**
* Formats the expiration date from the provided payment details.
*
* PayPal requires the expiration date to be in MMYY format.
* Using a four-digit year (MMYYYY) will cause some banks to decline
* transactions because the expiration date is considered invalid.
* For example, 072018 will be interpreted as 0720 instead of 0718.
*
* @param array $payment_details
* The payment details.
*
* @return string
* The expiration date, in the MMYY format.
*/
protected function getExpirationDate(array $payment_details) {
$date = \DateTime::createFromFormat('Y', $payment_details['expiration']['year']);
return $payment_details['expiration']['month'] . $date->format('y');
}
/**
* Merge default Payflow parameters in with the provided ones.
*
* @param array $parameters
* The parameters for the transaction.
*
* @return array
* The new parameters.
*/
protected function getParameters(array $parameters = []) {
$defaultParameters = [
'tender' => 'C',
'partner' => $this->getPartner(),
'vendor' => $this->getVendor(),
'user' => $this->getUser(),
'pwd' => $this->getPassword(),
];
return $parameters + $defaultParameters;
}
/**
* Get the remote transaction number ('pnref') of a payment.
*
* @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
* The payment.
*
* @return string
* The Payflow transaction number.
*/
protected function getTransactionNumber(PaymentInterface $payment) {
return explode('|', $payment->getRemoteId())[0];
}
/**
* Get the remote authorization code ('authcode') of a payment.
*
* @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
* The payment.
*
* @return string
* The Payflow authorization code.
*/
protected function getAuthorizationCode(PaymentInterface $payment) {
$remote_id = $payment->getRemoteId();
return (str_contains($remote_id, '|')) ? explode('|', $remote_id)[1] : $remote_id;
}
/**
* Gets a composite Remote ID from two Payflow payment transaction fields.
*
* @param array $data
* A data array including 'pnref' and 'authcode' keys.
*
* @return bool|string
* FALSE if required keys are missing, or a string "<pnref>|<authcode>".
*/
protected function prepareRemoteId(array $data) {
if (!array_key_exists('pnref', $data) || !array_key_exists('authcode', $data)) {
return FALSE;
}
return $data['pnref'] . '|' . $data['authcode'];
}
/**
* Prepares the request body to name/value pairs.
*
* @param array $parameters
* The request parameters.
*
* @return string
* The request body.
*/
protected function prepareBody(array $parameters = []) {
$parameters = $this->getParameters($parameters);
$values = [];
foreach ($parameters as $key => $value) {
$values[] = strtoupper($key) . '=' . $value;
}
return implode('&', $values);
}
/**
* Prepares the result of a request.
*
* @param string $body
* The result.
*
* @return array
* An array of the result values.
*/
protected function prepareResult($body) {
$responseParts = explode('&', $body);
$result = [];
foreach ($responseParts as $bodyPart) {
[$key, $value] = explode('=', $bodyPart, 2);
$result[strtolower($key)] = $value;
}
return $result;
}
/**
* Post a transaction to the Payflow server and return the response.
*
* @param array $parameters
* The parameters to send (will have base parameters added).
*
* @return array
* The response body data in array format.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function executeTransaction(array $parameters) {
$body = $this->prepareBody($parameters);
$response = $this->httpClient->post($this->getApiUrl(), [
'headers' => [
'Content-Type' => 'text/namevalue',
'Content-Length' => strlen($body),
],
'body' => $body,
'timeout' => 0,
]);
return $this->prepareResult($response->getBody()->getContents());
}
/**
* Attempt to validate payment information according to a payment state.
*
* @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
* The payment to validate.
* @param string|null $payment_state
* The payment state to validate the payment for.
*/
protected function validatePayment(PaymentInterface $payment, $payment_state = 'new') {
$this->assertPaymentState($payment, [$payment_state]);
$payment_method = $payment->getPaymentMethod();
if (empty($payment_method)) {
throw new \InvalidArgumentException('The provided payment has no payment method referenced.');
}
switch ($payment_state) {
case 'new':
if ($payment_method->isExpired()) {
throw new HardDeclineException('The provided payment method has expired.');
}
break;
case 'authorization':
if ($payment->isExpired()) {
throw new \InvalidArgumentException('Authorizations are guaranteed for up to 29 days.');
}
if (empty($payment->getRemoteId())) {
throw new \InvalidArgumentException('Could not retrieve the transaction ID.');
}
break;
}
}
/**
* {@inheritdoc}
*/
public function createPayment(PaymentInterface $payment, $capture = TRUE) {
$this->validatePayment($payment, 'new');
try {
$params = [
'trxtype' => $capture ? 'S' : 'A',
'amt' => $this->rounder->round($payment->getAmount())->getNumber(),
'currencycode' => $payment->getAmount()->getCurrencyCode(),
'origid' => $payment->getPaymentMethod()->getRemoteId(),
'verbosity' => 'HIGH',
// 'orderid' => $payment->getOrderId(),
];
$event = new PayflowRequestEvent($payment->getOrder(), $params);
$this->eventDispatcher->dispatch($event, PayPalEvents::PAYFLOW_CREATE_PAYMENT);
$data = $this->executeTransaction($event->getParams());
if ($data['result'] !== '0') {
throw new HardDeclineException('Could not charge the payment method. Response: ' . $data['respmsg'], $data['result']);
}
$next_state = $capture ? 'completed' : 'authorization';
$payment->setState($next_state);
if (!$capture) {
$payment->setExpiresTime($this->time->getRequestTime() + (86400 * 29));
}
$payment
->setRemoteId($this->prepareRemoteId($data))
->setRemoteState('3')
->save();
}
catch (RequestException $e) {
throw new HardDeclineException('Could not charge the payment method.');
}
}
/**
* {@inheritdoc}
*/
public function capturePayment(PaymentInterface $payment, ?Price $amount = NULL) {
$this->validatePayment($payment, 'authorization');
// If not specified, capture the entire amount.
$amount = $amount ?: $payment->getAmount();
$amount = $this->rounder->round($amount);
try {
$data = $this->executeTransaction([
'trxtype' => 'D',
'amt' => $this->rounder->round($amount)->getNumber(),
'currency' => $amount->getCurrencyCode(),
'origid' => $this->getTransactionNumber($payment),
]);
if ($data['result'] !== '0') {
throw new PaymentGatewayException('Could not capture payment. Message: ' . $data['respmsg'], $data['result']);
}
$payment->setState('completed');
$payment->setAmount($amount);
$payment->save();
}
catch (RequestException $e) {
throw new PaymentGatewayException('Could not capture payment. Message: ' . $e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function voidPayment(PaymentInterface $payment) {
$this->validatePayment($payment, 'authorization');
$remoteId = $this->getTransactionNumber($payment);
if (empty($remoteId)) {
throw new PaymentGatewayException('Remote authorization ID could not be determined.');
}
try {
$data = $this->executeTransaction([
'trxtype' => 'V',
'origid' => $this->getTransactionNumber($payment),
'verbosity' => 'HIGH',
]);
if ($data['result'] !== '0') {
throw new PaymentGatewayException('Payment could not be voided. Message: ' . $data['respmsg'], $data['result']);
}
$payment->setState('authorization_voided');
$payment->save();
}
catch (RequestException $e) {
throw new \InvalidArgumentException('Only payments in the "authorization" state can be voided.');
}
}
/**
* {@inheritdoc}
*/
public function refundPayment(PaymentInterface $payment, ?Price $amount = NULL) {
$this->assertPaymentState($payment, ['completed', 'partially_refunded']);
if ($payment->getCompletedTime() < strtotime('-180 days')) {
throw new InvalidRequestException("Unable to refund a payment captured more than 180 days ago.");
}
// If not specified, refund the entire amount.
$amount = $amount ?: $payment->getAmount();
$amount = $this->rounder->round($amount);
$this->assertRefundAmount($payment, $amount);
$transaction_number = $this->getTransactionNumber($payment);
if (empty($transaction_number)) {
throw new InvalidRequestException('Could not determine the remote payment details.');
}
try {
$new_refunded_amount = $payment->getRefundedAmount()->add($amount);
$data = $this->executeTransaction([
'trxtype' => 'C',
'origid' => $transaction_number,
'amt' => $amount->getNumber(),
]);
if ($data['result'] !== '0') {
throw new PaymentGatewayException('Credit could not be completed. Message: ' . $data['respmsg'], $data['result']);
}
if ($new_refunded_amount->lessThan($payment->getAmount())) {
$payment->setState('partially_refunded');
}
else {
$payment->setState('refunded');
}
$payment->setRefundedAmount($new_refunded_amount);
$payment->save();
}
catch (RequestException $e) {
throw new InvalidRequestException("Could not refund the payment.", $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) {
try {
/** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */
$address = $payment_method->getBillingProfile()->get('address')->first();
$data = $this->executeTransaction([
'trxtype' => 'A',
'amt' => 0,
'verbosity' => 'HIGH',
'acct' => $payment_details['number'],
'expdate' => $this->getExpirationDate($payment_details),
'cvv2' => $payment_details['security_code'],
'billtoemail' => $payment_method->getOwner()->getEmail(),
'billtofirstname' => $address->getGivenName(),
'billtolastname' => $address->getFamilyName(),
'billtostreet' => $address->getAddressLine1(),
'billtocity' => $address->getLocality(),
'billtostate' => $address->getAdministrativeArea(),
'billtozip' => $address->getPostalCode(),
'billtocountry' => $address->getCountryCode(),
]);
if ($data['result'] !== '0') {
throw new HardDeclineException("Unable to verify the credit card: " . $data['respmsg'], $data['result']);
}
$payment_method->card_type = $payment_details['type'];
// Only the last 4 numbers are safe to store.
$payment_method->card_number = substr($payment_details['number'], -4);
$payment_method->card_exp_month = $payment_details['expiration']['month'];
$payment_method->card_exp_year = $payment_details['expiration']['year'];
$expires = CreditCard::calculateExpirationTimestamp($payment_details['expiration']['month'], $payment_details['expiration']['year']);
// Store the remote ID returned by the request.
$payment_method
->setRemoteId($data['pnref'])
->setExpiresTime($expires)
->save();
}
catch (RequestException $e) {
throw new HardDeclineException("Unable to store the credit card");
}
}
/**
* {@inheritdoc}
*/
public function deletePaymentMethod(PaymentMethodInterface $payment_method) {
$payment_method->delete();
}
}
