class Checkout in Commerce PayPal 8
Provides the PayPal Checkout payment gateway.
Plugin annotation
@CommercePaymentGateway(
id = "paypal_checkout",
label = @Translation("PayPal Checkout (Preferred)"),
display_label = @Translation("PayPal"),
modes = {
"test" = @Translation("Sandbox"),
"live" = @Translation("Live"),
},
forms = {
"add-payment-method" = "Drupal\commerce_paypal\PluginForm\Checkout\PaymentMethodAddForm",
"offsite-payment" = "Drupal\commerce_paypal\PluginForm\Checkout\PaymentOffsiteForm",
},
credit_card_types = {
"amex", "dinersclub", "discover", "jcb", "maestro", "mastercard", "visa", "unionpay"
},
payment_method_types = {"paypal_checkout"},
payment_type = "paypal_checkout",
requires_billing_information = FALSE,
)
Hierarchy
- class \Drupal\Component\Plugin\PluginBase implements DerivativeInspectionInterface, PluginInspectionInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
- class \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayBase implements PaymentGatewayInterface, ContainerFactoryPluginInterface uses PluginWithFormsTrait
- class \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase implements OffsitePaymentGatewayInterface
- class \Drupal\commerce_paypal\Plugin\Commerce\PaymentGateway\Checkout implements CheckoutInterface
- class \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase implements OffsitePaymentGatewayInterface
- class \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayBase implements PaymentGatewayInterface, ContainerFactoryPluginInterface uses PluginWithFormsTrait
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
Expanded class hierarchy of Checkout
See also
https://developer.paypal.com/docs/business/checkout/
File
- src/
Plugin/ Commerce/ PaymentGateway/ Checkout.php, line 49
Namespace
Drupal\commerce_paypal\Plugin\Commerce\PaymentGatewayView source
class Checkout extends OffsitePaymentGatewayBase implements CheckoutInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The PayPal Checkout SDK factory.
*
* @var \Drupal\commerce_paypal\CheckoutSdkFactoryInterface
*/
protected $checkoutSdkFactory;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->moduleHandler = $container
->get('module_handler');
$instance->checkoutSdkFactory = $container
->get('commerce_paypal.checkout_sdk_factory');
$instance->logger = $container
->get('logger.channel.commerce_paypal');
$instance->state = $container
->get('state');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'payment_solution' => 'smart_payment_buttons',
'client_id' => '',
'secret' => '',
'intent' => 'capture',
'disable_funding' => [],
'disable_card' => [],
'shipping_preference' => 'get_from_file',
'update_billing_profile' => TRUE,
'update_shipping_profile' => TRUE,
'style' => [],
'enable_on_cart' => TRUE,
'collect_billing_information' => FALSE,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$documentation_url = Url::fromUri('https://www.drupal.org/node/3042053')
->toString();
$form['mode']['#weight'] = 0;
$form['payment_solution'] = [
'#type' => 'select',
'#title' => $this
->t('PayPal Commerce Platform features'),
'#options' => [
'smart_payment_buttons' => $this
->t('Accept PayPal with Smart Payment Buttons'),
'custom_card_fields' => $this
->t('Accept credit cards'),
],
'#default_value' => $this->configuration['payment_solution'],
'#weight' => 0,
];
// Some settings are visible only when the "Smart Payment Buttons" payment
// solution is selected.
$spb_states = [
'visible' => [
':input[name="configuration[' . $this->pluginId . '][payment_solution]"]' => [
'value' => 'smart_payment_buttons',
],
],
];
$form['credentials'] = [
'#type' => 'fieldset',
'#title' => $this
->t('API Credentials'),
'#weight' => 0,
];
$form['credentials']['help'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => [
'class' => [
'form-item',
],
],
'#value' => $this
->t('Refer to the <a href=":url" target="_blank">module documentation</a> to find your API credentials.', [
':url' => $documentation_url,
]),
];
$form['credentials']['client_id'] = [
'#type' => 'textfield',
'#title' => $this
->t('Client ID'),
'#default_value' => $this->configuration['client_id'],
'#maxlength' => 255,
'#required' => TRUE,
'#parents' => array_merge($form['#parents'], [
'client_id',
]),
];
$form['credentials']['secret'] = [
'#type' => 'textfield',
'#title' => $this
->t('Secret'),
'#maxlength' => 255,
'#default_value' => $this->configuration['secret'],
'#required' => TRUE,
'#parents' => array_merge($form['#parents'], [
'secret',
]),
];
$form['collect_billing_information']['#field_suffix'] = $this
->t('Collect billing information');
$form['collect_billing_information']['#description'] = $this
->t('When disabled, PayPal will collect the billing information instead, in the opened modal.');
$form['collect_billing_information']['#states'] = $spb_states;
$form['collect_billing_information']['#title_display'] = 'before';
$form['collect_billing_information']['#title'] = $this
->t('General');
$form['enable_on_cart'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Show Smart Payment Buttons on the cart page.'),
'#default_value' => $this->configuration['enable_on_cart'],
'#states' => $spb_states,
];
$form['intent'] = [
'#type' => 'radios',
'#title' => $this
->t('Transaction type'),
'#options' => [
'capture' => $this
->t("Capture (capture payment immediately after customer's approval)"),
'authorize' => $this
->t('Authorize (requires manual or automated capture after checkout)'),
],
'#description' => $this
->t('For more information on capturing a prior authorization, please refer to <a href=":url" target="_blank">Capture an authorization</a>.', [
':url' => 'https://docs.drupalcommerce.org/commerce2/user-guide/payments/capture',
]),
'#default_value' => $this->configuration['intent'],
];
$form['disable_funding'] = [
'#title' => $this
->t('Disable funding sources'),
'#description' => $this
->t('The disabled funding sources for the transaction. Any funding sources passed do not display with Smart Payment Buttons. By default, funding source eligibility is smartly decided based on a variety of factors.'),
'#type' => 'checkboxes',
'#options' => [
'card' => $this
->t('Credit or Debit Cards'),
'credit' => $this
->t('PayPal Credit'),
'sepa' => $this
->t('SEPA-Lastschrift'),
'sofort' => $this
->t('Sofort'),
'mybank' => $this
->t('MyBank'),
],
'#default_value' => $this->configuration['disable_funding'],
'#states' => $spb_states,
];
$form['disable_card'] = [
'#title' => $this
->t('Disable card types'),
'#description' => $this
->t('The disabled cards for the transaction. Any cards passed do not display with Smart Payment Buttons. By default, card eligibility is smartly decided based on a variety of factors.'),
'#type' => 'checkboxes',
'#options' => [
'visa' => $this
->t('Visa'),
'mastercard' => $this
->t('Mastercard'),
'amex' => $this
->t('American Express'),
'discover' => $this
->t('Discover'),
'jcb' => $this
->t('JCB'),
'elo' => $this
->t('Elo'),
'hiper' => $this
->t('Hiper'),
],
'#default_value' => $this->configuration['disable_card'],
];
$shipping_enabled = $this->moduleHandler
->moduleExists('commerce_shipping');
$form['shipping_preference'] = [
'#type' => 'radios',
'#title' => $this
->t('Shipping address collection'),
'#options' => [
'no_shipping' => $this
->t('Do not ask for a shipping address at PayPal.'),
'get_from_file' => $this
->t('Ask for a shipping address at PayPal even if the order already has one.'),
'set_provided_address' => $this
->t('Ask for a shipping address at PayPal if the order does not have one yet.'),
],
'#default_value' => $this->configuration['shipping_preference'],
'#access' => $shipping_enabled,
'#states' => $spb_states,
];
$form['update_billing_profile'] = [
'#type' => 'checkbox',
'#title' => t('Update the billing customer profile with address information the customer enters at PayPal.'),
'#default_value' => $this->configuration['update_billing_profile'],
'#states' => $spb_states,
];
$form['update_shipping_profile'] = [
'#type' => 'checkbox',
'#title' => t('Update shipping customer profiles with address information the customer enters at PayPal.'),
'#default_value' => $this->configuration['update_shipping_profile'],
'#access' => $shipping_enabled,
'#states' => $spb_states,
];
$form['customize_buttons'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Smart Payment Buttons style'),
'#default_value' => !empty($this->configuration['style']),
'#title_display' => 'before',
'#field_suffix' => $this
->t('Customize view'),
'#description_display' => 'before',
'#states' => $spb_states,
];
$form['style'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Settings'),
'#description' => $this
->t('For more information, please visit <a href=":url" target="_blank">customize the PayPal buttons</a>.', [
':url' => 'https://developer.paypal.com/docs/checkout/integration-features/customize-button/#layout',
]),
'#states' => array_merge_recursive($spb_states, [
'visible' => [
':input[name="configuration[' . $this->pluginId . '][customize_buttons]"]' => [
'checked' => TRUE,
],
],
]),
];
// Define some default values for the style configuration.
$this->configuration['style'] += [
'layout' => 'vertical',
'color' => 'gold',
'shape' => 'rect',
'label' => 'paypal',
'tagline' => FALSE,
];
$form['style']['layout'] = [
'#type' => 'select',
'#title' => $this
->t('Layout'),
'#default_value' => $this->configuration['style']['layout'],
'#options' => [
'vertical' => $this
->t('Vertical (Recommended)'),
'horizontal' => $this
->t('Horizontal'),
],
];
$form['style']['color'] = [
'#type' => 'select',
'#title' => $this
->t('Color'),
'#options' => [
'gold' => $this
->t('Gold (Recommended)'),
'blue' => $this
->t('Blue'),
'silver' => $this
->t('Silver'),
],
'#default_value' => $this->configuration['style']['color'],
];
$form['style']['shape'] = [
'#type' => 'select',
'#title' => $this
->t('Shape'),
'#options' => [
'rect' => $this
->t('Rect (Default)'),
'pill' => $this
->t('Pill'),
],
'#default_value' => $this->configuration['style']['shape'],
];
$form['style']['label'] = [
'#type' => 'select',
'#title' => $this
->t('Label'),
'#options' => [
'paypal' => $this
->t('Displays the PayPal logo (Default)'),
'checkout' => $this
->t('Displays the PayPal Checkout button'),
'buynow' => $this
->t('Displays the PayPal Buy Now button'),
'pay' => $this
->t('Displays the Pay With PayPal button'),
],
'#default_value' => $this->configuration['style']['label'],
];
$form['style']['tagline'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Display tagline'),
'#default_value' => $this->configuration['style']['tagline'],
'#states' => array_merge_recursive($spb_states, [
'visible' => [
':input[name="configuration[' . $this->pluginId . '][style][layout]"]' => [
'value' => 'horizontal',
],
],
]),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
if ($form_state
->getErrors()) {
return;
}
$values = $form_state
->getValue($form['#parents']);
if (empty($values['client_id']) || empty($values['secret'])) {
return;
}
$sdk = $this->checkoutSdkFactory
->get($values);
// Make sure we query for a fresh access token.
$this->state
->delete('commerce_paypal.oauth2_token');
try {
$sdk
->getAccessToken();
$this
->messenger()
->addMessage($this
->t('Connectivity to PayPal successfully verified.'));
} catch (BadResponseException $exception) {
$this
->messenger()
->addError($this
->t('Invalid client_id or secret specified.'));
$form_state
->setError($form['credentials']['client_id']);
$form_state
->setError($form['credentials']['secret']);
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
if ($form_state
->getErrors()) {
return;
}
$values = $form_state
->getValue($form['#parents']);
$values['disable_funding'] = array_filter($values['disable_funding']);
$values['disable_card'] = array_filter($values['disable_card']);
$keys = [
'payment_solution',
'client_id',
'secret',
'intent',
'disable_funding',
'disable_card',
'shipping_preference',
'update_billing_profile',
'update_shipping_profile',
'enable_on_cart',
];
// Only save the style settings if the customize buttons checkbox is checked.
if (!empty($values['customize_buttons'])) {
$keys[] = 'style';
// Can't display the tagline if the layout configured is "vertical".
if ($values['style']['layout'] === 'vertical') {
$values['style']['tagline'] = FALSE;
}
}
// When the "card" funding source is disabled, the "disable_card" setting
// cannot be specified.
if (isset($values['disable_funding']['card'])) {
$values['disable_card'] = [];
}
foreach ($keys as $key) {
if (!isset($values[$key])) {
continue;
}
$this->configuration[$key] = $values[$key];
}
}
/**
* {@inheritdoc}
*/
public function collectsBillingInformation() {
// Collecting a billing profile is required when selecting the
// "PayPal custom card fields" payment solution.
if ($this
->getPaymentSolution() == 'custom_card_fields') {
return TRUE;
}
return parent::collectsBillingInformation();
}
/**
* {@inheritdoc}
*/
public function getPaymentSolution() {
return $this->configuration['payment_solution'];
}
/**
* {@inheritdoc}
*/
public function createPayment(PaymentInterface $payment, $capture = TRUE) {
$payment_method = $payment
->getPaymentMethod();
if (!$payment_method || empty($payment_method
->getRemoteId())) {
throw new PaymentGatewayException('Cannot create the payment without the PayPal order ID.');
}
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
$order = $payment
->getOrder();
$checkout_data = $order
->getData('commerce_paypal_checkout', [
'flow' => '',
]);
$remote_id = $payment_method
->getRemoteId();
try {
// Ensure the PayPal order is up to date and in sync with Drupal.
$sdk
->updateOrder($remote_id, $order);
$request = $sdk
->getOrder($remote_id);
$paypal_order = Json::decode($request
->getBody());
} catch (BadResponseException $exception) {
throw new PaymentGatewayException($exception
->getMessage());
}
// When in the "shortcut" flow, the PayPal order status is expected to be
// "approved".
if ($checkout_data['flow'] === 'shortcut' && !in_array($paypal_order['status'], [
'APPROVED',
'SAVED',
])) {
throw new PaymentGatewayException(sprintf('Wrong remote order status. Expected: "approved"|"saved", Actual: %s.', $paypal_order['status']));
}
$intent = $checkout_data['intent'] ?? $this->configuration['intent'];
try {
if ($intent == 'capture') {
$response = $sdk
->captureOrder($remote_id);
$paypal_order = Json::decode($response
->getBody()
->getContents());
$remote_payment = $paypal_order['purchase_units'][0]['payments']['captures'][0];
$payment
->setRemoteId($remote_payment['id']);
}
else {
$response = $sdk
->authorizeOrder($remote_id);
$paypal_order = Json::decode($response
->getBody()
->getContents());
$remote_payment = $paypal_order['purchase_units'][0]['payments']['authorizations'][0];
if (isset($remote_payment['expiration_time'])) {
$expiration = new \DateTime($remote_payment['expiration_time']);
$payment
->setExpiresTime($expiration
->getTimestamp());
}
}
} catch (BadResponseException $exception) {
throw new PaymentGatewayException($exception
->getMessage());
}
$remote_state = strtolower($remote_payment['status']);
if (in_array($remote_state, [
'denied',
'expired',
'declined',
])) {
throw new HardDeclineException(sprintf('Could not %s the payment for order %s. Remote payment state: %s', $intent, $order
->id(), $remote_state));
}
$state = $this
->mapPaymentState($intent, $remote_state);
// If we couldn't find a state to map to, stop here.
if (!$state) {
$this->logger
->debug('PayPal remote payment debug: <pre>@remote_payment</pre>', [
'@remote_payment' => print_r($remote_payment, TRUE),
]);
throw new PaymentGatewayException(sprintf('The PayPal payment is in a state we cannot handle. Remote state: %s.', $remote_state));
}
// Special handling of the "pending" state, if the order is "pending review"
// we allow the order to go "through" to give a chance to the merchant
// to accept the payment, in case manual review is needed.
if ($state === 'pending' && $remote_state === 'pending') {
$reason = $remote_payment['status_details']['reason'];
if ($reason === 'PENDING_REVIEW') {
$state = 'authorization';
}
else {
throw new PaymentGatewayException(sprintf('The PayPal payment is pending. Reason: %s.', $reason));
}
}
$payment_amount = Price::fromArray([
'number' => $remote_payment['amount']['value'],
'currency_code' => $remote_payment['amount']['currency_code'],
]);
$payment
->setAmount($payment_amount);
$payment
->setState($state);
$payment
->setRemoteId($remote_payment['id']);
$payment
->setRemoteState($remote_state);
$payment
->save();
}
/**
* {@inheritdoc}
*/
public function capturePayment(PaymentInterface $payment, Price $amount = NULL) {
$this
->assertPaymentState($payment, [
'authorization',
]);
// If not specified, capture the entire amount.
$amount = $amount ?: $payment
->getAmount();
$remote_id = $payment
->getRemoteId();
$params = [
'amount' => [
'value' => Calculator::trim($amount
->getNumber()),
'currency_code' => $amount
->getCurrencyCode(),
],
];
if ($amount
->equals($payment
->getAmount())) {
$params['final_capture'] = TRUE;
}
try {
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
// If the payment was authorized more than 3 days ago, attempt to
// reauthorize it.
if ($this->time
->getRequestTime() >= $payment
->getAuthorizedTime() + 86400 * 3 && !$payment
->isExpired()) {
$sdk
->reAuthorizePayment($remote_id, [
'amount' => $params['amount'],
]);
}
$response = $sdk
->capturePayment($remote_id, $params);
$response = Json::decode($response
->getBody()
->getContents());
} catch (BadResponseException $exception) {
$this->logger
->error($exception
->getResponse()
->getBody()
->getContents());
throw new PaymentGatewayException('An error occurred while capturing the authorized payment.');
}
$remote_state = strtolower($response['status']);
$state = $this
->mapPaymentState('capture', $remote_state);
if (!$state) {
throw new PaymentGatewayException('Unhandled payment state.');
}
$payment
->setState('completed');
$payment
->setAmount($amount);
$payment
->setRemoteId($response['id']);
$payment
->setRemoteState($remote_state);
$payment
->save();
}
/**
* {@inheritdoc}
*/
public function voidPayment(PaymentInterface $payment) {
$this
->assertPaymentState($payment, [
'authorization',
]);
try {
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
$response = $sdk
->voidPayment($payment
->getRemoteId());
} catch (BadResponseException $exception) {
$this->logger
->error($exception
->getResponse()
->getBody()
->getContents());
throw new PaymentGatewayException('An error occurred while voiding the payment.');
}
if ($response
->getStatusCode() == Response::HTTP_NO_CONTENT) {
$payment
->setState('authorization_voided');
$payment
->save();
}
}
/**
* {@inheritdoc}
*/
public function refundPayment(PaymentInterface $payment, Price $amount = NULL) {
$this
->assertPaymentState($payment, [
'completed',
'partially_refunded',
]);
// If not specified, refund the entire amount.
$amount = $amount ?: $payment
->getAmount();
$this
->assertRefundAmount($payment, $amount);
$old_refunded_amount = $payment
->getRefundedAmount();
$new_refunded_amount = $old_refunded_amount
->add($amount);
$params = [
'amount' => [
'value' => Calculator::trim($amount
->getNumber()),
'currency_code' => $amount
->getCurrencyCode(),
],
];
if ($new_refunded_amount
->lessThan($payment
->getAmount())) {
$payment
->setState('partially_refunded');
}
else {
$payment
->setState('refunded');
}
try {
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
$response = $sdk
->refundPayment($payment
->getRemoteId(), $params);
$response = Json::decode($response
->getBody()
->getContents());
} catch (BadResponseException $exception) {
$this->logger
->error($exception
->getResponse()
->getBody()
->getContents());
throw new PaymentGatewayException('An error occurred while refunding the payment.');
}
if (strtolower($response['status']) !== 'completed') {
throw new PaymentGatewayException(sprintf('Invalid state returned by PayPal. Expected: ("%s"), Actual: ("%s").', 'COMPLETED', $response['status']));
}
$payment
->setRemoteState($response['status']);
$payment
->setRefundedAmount($new_refunded_amount);
$payment
->save();
}
/**
* {@inheritdoc}
*/
public function onReturn(OrderInterface $order, Request $request) {
try {
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
$paypal_request = $sdk
->getOrder($order
->getData('paypal_order_id'));
$paypal_order = Json::decode($paypal_request
->getBody());
} catch (BadResponseException $exception) {
throw new PaymentGatewayException('Could not load the order from PayPal.');
}
$paypal_amount = $paypal_order['purchase_units'][0]['amount'];
$paypal_total = Price::fromArray([
'number' => $paypal_amount['value'],
'currency_code' => $paypal_amount['currency_code'],
]);
// Make sure the order total matches the total we get from PayPal.
if (!$paypal_total
->equals($order
->getTotalPrice())) {
throw new PaymentGatewayException('The PayPal order total does not match the order total.');
}
if (!in_array($paypal_order['status'], [
'APPROVED',
'SAVED',
])) {
throw new PaymentGatewayException(sprintf('Unexpected PayPal order status %s.', $paypal_order['status']));
}
$flow = $order
->getData('paypal_checkout_flow');
$order
->setData('commerce_paypal_checkout', [
'remote_id' => $paypal_order['id'],
'flow' => $flow,
'intent' => strtolower($paypal_order['intent']),
]);
if (empty($order
->getEmail())) {
$order
->setEmail($paypal_order['payer']['email_address']);
}
if ($this->configuration['update_billing_profile']) {
$this
->updateProfile($order, 'billing', $paypal_order);
}
if (!empty($this->configuration['update_shipping_profile']) && $order
->hasField('shipments')) {
$this
->updateProfile($order, 'shipping', $paypal_order);
}
$payment_method = NULL;
// If a payment method is already referenced by the order, no need to create
// a new one.
if (!$order
->get('payment_method')
->isEmpty()) {
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
$payment_method = $order
->get('payment_method')->entity;
}
// If the order doesn't reference a payment method yet, or if the payment
// method doesn't reference the right gateway, create a new one.
if (!$payment_method || $payment_method
->getPaymentGatewayId() !== $this->parentEntity
->id()) {
// Create a payment method.
$payment_method_storage = $this->entityTypeManager
->getStorage('commerce_payment_method');
assert($payment_method_storage instanceof PaymentMethodStorageInterface);
$payment_method = $payment_method_storage
->createForCustomer('paypal_checkout', $this->parentEntity
->id(), $order
->getCustomerId(), $order
->getBillingProfile());
}
$payment_method
->setRemoteId($paypal_order['id']);
$payment_method
->setReusable(FALSE);
$payment_method
->save();
$order
->set('payment_method', $payment_method);
if ($flow === 'shortcut' && $order
->hasField('checkout_flow')) {
// Force the checkout flow to PayPal checkout which is the flow the module
// defines for the "shortcut" flow.
$order
->set('checkout_flow', 'paypal_checkout');
$order
->set('checkout_step', NULL);
}
// For the "mark" flow, create the payment right away (if not configured
// to be skipped).
if ($flow === 'mark' && !$request->query
->has('skip_payment_creation')) {
$payment_storage = $this->entityTypeManager
->getStorage('commerce_payment');
/** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
$payment = $payment_storage
->create([
'state' => 'new',
'amount' => $order
->getBalance(),
'payment_gateway' => $this->parentEntity
->id(),
'payment_method' => $payment_method
->id(),
'order_id' => $order
->id(),
]);
$this
->createPayment($payment);
}
}
/**
* {@inheritdoc}
*/
public function createPaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) {
if (empty($payment_details['paypal_remote_id'])) {
throw new PaymentGatewayException('Cannot create the payment method without the PayPal order ID.');
}
try {
$sdk = $this->checkoutSdkFactory
->get($this->configuration);
$request = $sdk
->getOrder($payment_details['paypal_remote_id']);
$paypal_order = Json::decode($request
->getBody());
} catch (BadResponseException $exception) {
throw new PaymentGatewayException($exception
->getResponse()
->getBody()
->getContents());
}
// Check if we have information about the card used.
if (isset($paypal_order['payment_source']['card'])) {
$payment_source = $paypal_order['payment_source']['card'];
// Remove any character that isn't A-Z, a-z or 0-9.
$payment_source['brand'] = strtolower(preg_replace("/[^A-Za-z0-9]/", '', $payment_source['brand']));
// We should in theory map the credit card type we get from PayPal to one
// expected by us, but the credit card types are not correctly documented.
// For example, ("Mastercard" is sent as "MASTER_CARD" but documented
// as "MASTERCARD").
$card_types = CreditCard::getTypes();
if (!isset($card_types[$payment_source['brand']])) {
throw new HardDeclineException(sprintf('Unsupported credit card type "%s".', $paypal_order['payment_source']['card']));
}
$payment_method
->set('card_type', $payment_source['brand']);
$payment_method
->set('card_number', $payment_source['last_digits']);
}
$payment_method
->setRemoteId($paypal_order['id']);
$payment_method
->setReusable(FALSE);
$payment_method
->save();
}
/**
* {@inheritdoc}
*/
public function deletePaymentMethod(PaymentMethodInterface $payment_method) {
$payment_method
->delete();
}
/**
* Map a PayPal payment state to a local one.
*
* @param string $type
* The payment type. One of "authorize" or "capture".
* @param string $remote_state
* The PayPal remote payment state.
*
* @return string
* The corresponding local payment state.
*/
protected function mapPaymentState($type, $remote_state) {
$mapping = [
'authorize' => [
'created' => 'authorization',
'pending' => 'pending',
'voided' => 'authorization_voided',
'expired' => 'authorization_expired',
],
'capture' => [
'completed' => 'completed',
'pending' => 'pending',
'partially_refunded' => 'partially_refunded',
],
];
return isset($mapping[$type][$remote_state]) ? $mapping[$type][$remote_state] : '';
}
/**
* Updates the profile of the given type using the response returned by PayPal.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
* @param string $type
* The type (billing|profile).
* @param array $paypal_order
* The PayPal order.
*/
protected function updateProfile(OrderInterface $order, $type, array $paypal_order) {
if ($type == 'billing') {
/** @var \Drupal\profile\Entity\ProfileInterface $profile */
$profile = $order
->getBillingProfile() ?: $this
->buildCustomerProfile($order);
$profile->address->given_name = $paypal_order['payer']['name']['given_name'];
$profile->address->family_name = $paypal_order['payer']['name']['surname'];
if (isset($paypal_order['payer']['address'])) {
$this
->populateProfile($profile, $paypal_order['payer']['address']);
}
$profile
->save();
$order
->setBillingProfile($profile);
}
elseif ($type == 'shipping' && !empty($paypal_order['purchase_units'][0]['shipping'])) {
$shipping_info = $paypal_order['purchase_units'][0]['shipping'];
$shipments = $order->shipments
->referencedEntities();
if (!$shipments) {
/** @var \Drupal\commerce_shipping\PackerManagerInterface $packer_manager */
$packer_manager = \Drupal::service('commerce_shipping.packer_manager');
list($shipments) = $packer_manager
->packToShipments($order, $this
->buildCustomerProfile($order), $shipments);
}
// Can't proceed without shipments.
if (!$shipments) {
return;
}
/** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $first_shipment */
$first_shipment = $shipments[0];
/** @var \Drupal\profile\Entity\ProfileInterface $profile */
$profile = $first_shipment
->getShippingProfile() ?: $this
->buildCustomerProfile($order);
// This is a hack but shipments with empty amounts is crashing other
// contrib modules.
// Ideally, we shouldn't have to pack the shipments ourselves...
if (!$first_shipment
->getAmount()) {
$shipment_amount = Price::fromArray([
'number' => 0,
'currency_code' => $order
->getTotalPrice()
->getCurrencyCode(),
]);
$first_shipment
->setAmount($shipment_amount);
}
// We only get the full name from PayPal, so we need to "guess" the given
// name and the family name.
$names = explode(' ', $shipping_info['name']['full_name']);
$given_name = array_shift($names);
$family_name = implode(' ', $names);
$profile->address->given_name = $given_name;
$profile->address->family_name = $family_name;
if (!empty($shipping_info['address'])) {
$this
->populateProfile($profile, $shipping_info['address']);
}
$profile
->save();
$first_shipment
->setShippingProfile($profile);
$first_shipment
->save();
$order
->set('shipments', $shipments);
}
}
/**
* Builds a customer profile, assigned to the order's owner.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
*
* @return \Drupal\profile\Entity\ProfileInterface
* The customer profile.
*/
protected function buildCustomerProfile(OrderInterface $order) {
return $this->entityTypeManager
->getStorage('profile')
->create([
'uid' => $order
->getCustomerId(),
'type' => 'customer',
]);
}
/**
* Populate the given profile with the given PayPal address.
*
* @param \Drupal\profile\Entity\ProfileInterface $profile
* The profile to populate.
* @param array $address
* The PayPal address.
*/
protected function populateProfile(ProfileInterface $profile, array $address) {
// Map PayPal address keys to keys expected by AddressItem.
$mapping = [
'address_line_1' => 'address_line1',
'address_line_2' => 'address_line2',
'admin_area_1' => 'administrative_area',
'admin_area_2' => 'locality',
'postal_code' => 'postal_code',
'country_code' => 'country_code',
];
foreach ($address as $key => $value) {
if (!isset($mapping[$key])) {
continue;
}
// PayPal address fields have a higher maximum length than ours.
$value = $key == 'country_code' ? $value : mb_substr($value, 0, 255);
$profile->address->{$mapping[$key]} = $value;
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
Checkout:: |
protected | property | The PayPal Checkout SDK factory. | |
Checkout:: |
protected | property | The logger. | |
Checkout:: |
protected | property | The module handler. | |
Checkout:: |
protected | property | The state service. | |
Checkout:: |
public | function |
Form constructor. Overrides PaymentGatewayBase:: |
|
Checkout:: |
protected | function | Builds a customer profile, assigned to the order's owner. | |
Checkout:: |
public | function |
Captures the given authorized payment. Overrides SupportsAuthorizationsInterface:: |
|
Checkout:: |
public | function |
Gets whether the payment gateway collects billing information. Overrides PaymentGatewayBase:: |
|
Checkout:: |
public static | function |
Creates an instance of the plugin. Overrides PaymentGatewayBase:: |
|
Checkout:: |
public | function |
Creates a payment. Overrides SupportsStoredPaymentMethodsInterface:: |
|
Checkout:: |
public | function |
Creates a payment method with the given payment details. Overrides SupportsCreatingPaymentMethodsInterface:: |
|
Checkout:: |
public | function |
Gets default configuration for this plugin. Overrides PaymentGatewayBase:: |
|
Checkout:: |
public | function |
Deletes the given payment method. Overrides SupportsStoredPaymentMethodsInterface:: |
|
Checkout:: |
public | function |
Returns the payment solution (e.g "smart_payment_buttons"). Overrides CheckoutInterface:: |
|
Checkout:: |
protected | function | Map a PayPal payment state to a local one. | |
Checkout:: |
public | function |
Processes the "return" request. Overrides OffsitePaymentGatewayBase:: |
|
Checkout:: |
protected | function | Populate the given profile with the given PayPal address. | |
Checkout:: |
public | function |
Refunds the given payment. Overrides SupportsRefundsInterface:: |
|
Checkout:: |
public | function |
Form submission handler. Overrides PaymentGatewayBase:: |
|
Checkout:: |
protected | function | Updates the profile of the given type using the response returned by PayPal. | |
Checkout:: |
public | function |
Form validation handler. Overrides PaymentGatewayBase:: |
|
Checkout:: |
public | function |
Voids the given payment. Overrides SupportsVoidsInterface:: |
|
DependencySerializationTrait:: |
protected | property | An array of entity type IDs keyed by the property name of their storages. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
OffsitePaymentGatewayBase:: |
public | function |
Gets the URL to the "notify" page. Overrides OffsitePaymentGatewayInterface:: |
|
OffsitePaymentGatewayBase:: |
public | function |
Processes the "cancel" request. Overrides OffsitePaymentGatewayInterface:: |
|
OffsitePaymentGatewayBase:: |
public | function |
Processes the notification request. Overrides SupportsNotificationsInterface:: |
|
PaymentGatewayBase:: |
protected | property | The ID of the parent config entity. | |
PaymentGatewayBase:: |
protected | property | The entity type manager. | |
PaymentGatewayBase:: |
protected | property | The minor units converter. | |
PaymentGatewayBase:: |
protected | property | The parent config entity. | |
PaymentGatewayBase:: |
protected | property | The payment method types handled by the gateway. | |
PaymentGatewayBase:: |
protected | property | The payment type used by the gateway. | |
PaymentGatewayBase:: |
protected | property | The time. | |
PaymentGatewayBase:: |
protected | function | Asserts that the payment method is neither empty nor expired. | |
PaymentGatewayBase:: |
protected | function | Asserts that the payment state matches one of the allowed states. | |
PaymentGatewayBase:: |
protected | function | Asserts that the refund amount is valid. | |
PaymentGatewayBase:: |
public | function |
Builds a label for the given AVS response code and card type. Overrides PaymentGatewayInterface:: |
2 |
PaymentGatewayBase:: |
public | function |
Builds the available operations for the given payment. Overrides PaymentGatewayInterface:: |
1 |
PaymentGatewayBase:: |
public | function |
Calculates dependencies for the configured plugin. Overrides DependentPluginInterface:: |
|
PaymentGatewayBase:: |
public | function | ||
PaymentGatewayBase:: |
public | function | ||
PaymentGatewayBase:: |
public | function | ||
PaymentGatewayBase:: |
public | function |
Gets this plugin's configuration. Overrides ConfigurableInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the credit card types handled by the gateway. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
protected | function | Gets the default payment gateway forms. | 1 |
PaymentGatewayBase:: |
public | function |
Gets the default payment method type. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the payment gateway display label. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the JS library ID. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the payment gateway label. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the mode in which the payment gateway is operating. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the payment method types handled by the payment gateway. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Gets the payment type used by the payment gateway. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
protected | function | Gets the remote customer ID for the given user. | |
PaymentGatewayBase:: |
public | function |
Gets the supported modes. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Sets the configuration for this plugin instance. Overrides ConfigurableInterface:: |
|
PaymentGatewayBase:: |
protected | function | Sets the remote customer ID for the given user. | |
PaymentGatewayBase:: |
public | function |
Converts the given amount to its minor units. Overrides PaymentGatewayInterface:: |
|
PaymentGatewayBase:: |
public | function |
Constructs a new PaymentGatewayBase object. Overrides PluginBase:: |
3 |
PaymentGatewayBase:: |
public | function |
Overrides DependencySerializationTrait:: |
|
PaymentGatewayBase:: |
public | function |
Overrides DependencySerializationTrait:: |
|
PluginBase:: |
protected | property | Configuration information passed into the plugin. | 1 |
PluginBase:: |
protected | property | The plugin implementation definition. | 1 |
PluginBase:: |
protected | property | The plugin_id. | |
PluginBase:: |
constant | A string which is used to separate base plugin IDs from the derivative ID. | ||
PluginBase:: |
public | function |
Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the definition of the plugin implementation. Overrides PluginInspectionInterface:: |
3 |
PluginBase:: |
public | function |
Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface:: |
|
PluginBase:: |
public | function | Determines if the plugin is configurable. | |
PluginWithFormsTrait:: |
public | function | ||
PluginWithFormsTrait:: |
public | function | ||
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |