File: D:/HostingSpaces/SBogers95/rentman.io/app/Komma/Shop/Payment/PSPAdapters/MultiSafepay.php
<?php
namespace App\Komma\Shop\Payment\PSPAdapters;
use App\Komma\Globalization\RegionInfo;
use App\Komma\Shop\Orders\Models\Order;
use App\Komma\Shop\Orders\OrderStatus;
use App\Komma\Shop\Payment\Clients\MultiSafepay\Client;
use App\Komma\Shop\Payment\Clients\MultiSafepay\Enums\NotificationMethod;
use App\Komma\Shop\Payment\Clients\MultiSafepay\Enums\OrderStatus as MultiSafepayOrderStatus;
use App\Komma\Shop\Payment\Clients\MultiSafepay\Enums\PaymentFlow;
use App\Komma\Shop\Payment\Clients\MultiSafepay\Models\Order as MultiSafePayOrder;
use App\Komma\Shop\Payment\PaymentServiceInterface;
use App\Komma\Shop\Payment\Transaction;
use App\Komma\Shop\Payment\TransactionStatus;
use App\Komma\Users\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;
class MultiSafepay extends AbstractPSPAdapter
{
/** @var Client A client provided by the PSP to communicate with their api */
private $paymentServiceProviderClient;
/** @var string The url to the test environment from multisafepay */
private $testEnvironmentApiUrl = 'https://testapi.multisafepay.com/v1/json/';
/** @var string The url to the production environment from multisafepay */
private $productionEnvironmentApiUrl = 'https://api.multisafepay.com/v1/json/';
/** @var string The name of the website as to include in the external payment environment */
private $shopName;
/** @var string Your Google Analytics Site Id. This will be injected into the payment pages so you can trigger custom events and track payment metrics. */
private $googleAnalyticsKey;
/** @var string */
private $adapterVersion = '201901231409'; //Year month day hour minute version string that represents this adapters version. Will be sent to MultiSafepay
/**
* MultiSafePay constructor.
*
* @see PaymentServiceInterface::getAdapter()
* @param Client $apiClient
*/
public function __construct(Client $apiClient)
{
parent::__construct();
$this->pspName = 'multisafepay';
$apiKey = config('payment.payment_service_providers.'.$this->pspName.'.'.$this->environment.'.api_keys.key');
$this->shopName = config('payment.payment_service_providers.'.$this->pspName.'.'.$this->environment.'.shop_name');
$this->googleAnalyticsKey = config('payment.payment_service_providers.'.$this->pspName.'.'.$this->environment.'.google_analytics_key');
$this->paymentServiceProviderClient = $apiClient;
$this->paymentServiceProviderClient->setApiKey($apiKey);
switch (strtolower($this->environment)) {
case 'production':
$this->paymentServiceProviderClient->setApiUrl($this->productionEnvironmentApiUrl);
break;
case 'local':
default:
$this->paymentServiceProviderClient->setApiUrl($this->testEnvironmentApiUrl);
break;
}
}
/**
* Creates a payment transaction for a certain order.
*
* @param Order $order
* @return Transaction
* @throws \Exception
*/
public function createTransaction(Order $order): Transaction
{
/** @var User $customer */
$customer = $order->customer()->first();
$shopRegionInfo = new RegionInfo('NL'); //Shop was build with euros in mind and the Dutch language as the primary one
$customerRegionInfo = new RegionInfo($customer->culture);
//Check if the customers culture is supported by MultiSafepay. Else default to the english one
$userLocale = str_replace('-', '_', $customer->culture);
//Format all the data according to the spec of the psp
$paymentData = [
'type' => PaymentFlow::REDIRECT, //Optional string
'order_id' => $order->id, //Required string
'currency' => $shopRegionInfo->getISOCurrencySymbol(), //Required string
'amount' => (int) $order->total_price, //Required int. The amount (in cents) that the customer needs to pay.
'description' => $this->shopName.' order #'.$order->id, //A free text description which will be shown with the order in MultiSafepay Control. If the customers bank supports it this description will also be shown on the customers bank statement.
'items' => '', //Optional string. Include an HTML formatted list of the items ordered. Used to customer how the order is displayed on payment pages and emails.
'var1' => '', //Optional string. A free variable for custom data to be stored and persisted.
'var2' => '', //Optional string. A free variable for custom data to be stored and persisted.
'var3' => '', //Optional string. A free variable for custom data to be stored and persisted.
'manual' => 'false', //Optional boolean If true this forces a credit card transaction to require manual acceptance regardless of the outcome from fraud checks. It is possible that a high risk transaction is still declined.
'gateway' => '', //Optional string The unique gateway id to immediately direct the customer to the payment method. You retrieve these gateways using a gateway request.
'days_active' => '30', //Optional int The number of days the payment link will be active for it the flow is a payment link,
'payment_options' => [ //Required
'notification_url' => route('transaction.statusupdate', ['order' => $order->id]), //Optional string. The URL that MultiSafepay will send transaction status updates to when the status of a transaction changes. Notice: Api documentation states this url is used for updates. But it does not seem to in the traditional redirect flow.
'notification_method' => NotificationMethod::POST, //Optional string. MultiSafepay will send transaction status updates to “notification_url” using the method specified. Options: POST, GET
'redirect_url' => route('transaction.statusupdate', ['order' => $order->id]), //Optional string. Where multisafepay will direct a customer from multisafepay payment pages after a successful transaction.
'cancel_url' => route('transaction.statusupdate', ['order' => $order->id]), //Optional string. Where multisafepay will direct a customer from multisafepay payment pages after a cancelled or unsuccessful transaction.
'close_window' => 'true', //Optional string true or false. Set to true if you will display the MultiSafepay payment page in a new window and want it to close automatically after the payment process has been completed.
],
'customer' => [ //Optional array
'locale' => $userLocale, //Include to provide localized payment pages for the customer. Use the format ab_CD with ISO 639 language codes and ISO 3166 country codes.
'ip_address' => request()->ip(), //The IP address of the customer.
// "forwarded_ip" => "127.0.0.1", //The X-FORWARED-FOR header of the customer request when using a proxy.
'first_name' => $order->shipping_first_name, //Optional string. The customer’s first name. - (recommended)
'last_name' => $order->shipping_last_name, //Optional string. The customer’s last name. - (recommended)
'address1' => $order->shipping_street, //Optional string. First line of customer’s provided address. - (recommended)
'address2' => '', //Optional string. Second line of customer’s provided address.
'house_number' => $order->shipping_house_number, //Optional string Customer’s provided house number. - (recommended)
'zip_code' => $order->shipping_postal_code, //Optional string Customer’s provided zip / postal code. - (recommended)
'city' => $order->shipping_city, //Optional string Customer’s provided city. - (recommended)
'state' => '', //Optional string Customer’s provided city. - (recommended)
'country' => $customerRegionInfo->getTwoLetterISORegionName(), //Optional string Customer’s provided country code. (ISO 3166-1) - (recommended)
'phone' => $order->shipping_telephone, //Optional string. Customer’s provided phone number.
'email' => $order->shipping_email, //Optional string. Customer’s provided email address. Used to send second chance emails and in fraud checks. - (recommended)
],
'plugin' => [ //Optional array
'shop' => 'KMS Shop - MultiSafepay ', //Optional string. Shop name
'shop_version' => $this->adapterVersion, //Optional string. Shop version
'plugin_version' => $this->adapterVersion, //Optional string. Plugin version
'partner' => 'Komma B.V', //Optional string. Partner id
'shop_root_url' => URL::to('/'), //Optional string. The root url to the shop
],
];
if (! empty($this->googleAnalyticsKey)) {
$paymentData['google_analytics'] = [
'account' => $this->googleAnalyticsKey,
];
}
$multiSafePayOrderObject = $this->paymentServiceProviderClient->orders->post($paymentData);
$multiSafePayOrder = MultiSafePayOrder::FromJsonString(json_encode($multiSafePayOrderObject)); //The post method returns an ugly stdClass object.
$transaction = new Transaction();
$transaction->psp = $this->pspName;
$transaction->status = TransactionStatus::OPEN;
$transaction = $this->updateTransactionUsingMultiSafePayOrder($multiSafePayOrder, $transaction);
$transaction->order()->associate($order);
$transaction->ip = request()->ip();
$transaction->currency_iso_4217_code = $shopRegionInfo->getISOCurrencySymbol();
$transaction->amount = $order->total_price;
$transaction->save();
return $transaction;
}
/**
* Updates a kms shop transaction using the payment from a MultiSafepay transaction.
* Notice. You need to save the transaction yourself.
*
* @param MultiSafePayOrder $multiSafepayOrder
* @param Transaction $transaction
* @return Transaction
* @throws \Exception
*/
private function updateTransactionUsingMultiSafePayOrder(MultiSafePayOrder $multiSafepayOrder, Transaction $transaction): Transaction
{
if ($multiSafepayOrder->getPaymentUrl() != '') {
$transaction->payment_link = $multiSafepayOrder->getPaymentUrl();
}
if ($multiSafepayOrder->getTransactionId() != '') {
$transaction->psp_id = $multiSafepayOrder->getTransactionId();
} //You don't believe it but they don't always pass the transaction id. Especially when you just created it.
if ($multiSafepayOrder->getAmount() !== 0) {
$transaction->amount = $multiSafepayOrder->getAmount();
}
if ($multiSafepayOrder->getCurrency() !== '') {
$transaction->currency_iso_4217_code = $multiSafepayOrder->getCurrency();
}
if ($multiSafepayOrder->getStatus() !== '') {
$transaction->status = $this->adaptPSPTransactionStatusToKmsTransactionStatus($multiSafepayOrder->getStatus());
}
if ($multiSafepayOrder->getPaymentDetails()) {
if ($multiSafepayOrder->getPaymentDetails()->getType()) {
$transaction->payment_method = $multiSafepayOrder->getPaymentDetails()->getType();
}
if ($multiSafepayOrder->getPaymentDetails()->getAccountHolderName()) {
$transaction->account_holder_name = $multiSafepayOrder->getPaymentDetails()->getAccountHolderName();
}
if ($multiSafepayOrder->getPaymentDetails()->getAccountIban()) {
$transaction->account_reference = $multiSafepayOrder->getPaymentDetails()->getAccountIban();
}
}
return $transaction;
}
/**
* Updates the payment status for a certain order payment transaction.
*
* Must be triggered when the PSP has an update for a certain payment transaction.
* To indicate for example that a payment transaction was completed successfully.
*
* @param Order|null $order
* @return Response
*/
public function processPSPResponse(Order $order = null): Response
{
\Log::debug(' '); //Empty line in log
$transactionId = \Input::get('transactionid');
//Validate the request
if (! $transactionId) {
\Log::error('MultiSafepay Adapter: Expected multiSafepay to return a transactionId. But did not.');
return redirect()->to(route('transaction.view.exception', ['order' => $order]));
}
\Log::debug('MultiSafepay Adapter: MultiSafepay called kms with a status update for transaction with psp_id: '.$transactionId.'. Updating transaction....');
//Try to get the transaction by using the psp id
$transaction = Transaction::where('psp_id', '=', $transactionId)->where('psp', '=', $this->pspName)->first();
if (! $transaction) {
\Log::error('MultiSafepay Adapter: Could not find transaction with psp_id: '.$transactionId.' for psp: '.$this->pspName);
return redirect()->to(route('transaction.view.exception', ['order' => null])); //For security reasons we don't give it
}
//Try to retrieve the payment from MultiSafepay using the id they gave us
$multiSafepayOrder = null;
try {
//get the order
$multiSafepayOrderObject = $this->paymentServiceProviderClient->orders->get($endpoint = 'orders', $transactionId, $body = [], $query_string = false);
$multiSafepayOrder = MultiSafePayOrder::FromJsonString(json_encode($multiSafepayOrderObject));
\Log::debug('Got multisafepay order object from multisafepay:');
\Log::debug(json_encode($multiSafepayOrderObject));
if ($multiSafepayOrder->getOrderId() == '') {
\Log::error('The MultiSafepay order we got from multiSafepay did not have an order id. Therefore we cannot find and update the order and its transaction.');
return redirect()->to(route('transaction.view.exception', ['order' => $order]));
}
} catch (\Exception $e) {
\Log::error('MultiSafepay Adapter: Tried to get a payment with transactionId '.$transactionId.' from MultiSafepay. But an ApiException occured:');
\Log::error($e->getMessage());
\Log::error($e->getTraceAsString());
return redirect()->to(route('transaction.view.exception', ['order' => $order]));
}
//Try to update the transaction using the payment from MultiSafepay.
try {
$transaction = $this->updateTransactionUsingMultiSafePayOrder($multiSafepayOrder, $transaction);
$this->saveTransactionAndNotify($transaction);
} catch (\Exception $e) {
\Log::error('MultiSafepay Adapter: Tried update a kms transaction using a MultiSafepay order, but an exception occured: ');
\Log::error($e->getMessage());
\Log::error($e->getTraceAsString());
return redirect()->to(route('transaction.view.exception', ['order' => $order]));
}
\Log::debug('MultiSafepay Adapter: Transaction with order id "'.$multiSafepayOrder->getOrderId().'" successfully updated! Also telling that to MultiSafepay with http status 200 in a redirect. Transaction status: '.$transaction->status);
return redirect()->to(route('transaction.view.accepted', ['order' => $order])); //Status 200 let's MultiSafepay know we processed the request successfully, and that they don't need to trie again.
}
/**
* Return a response that redirects the user to a page
* where the user can select a payment method (credit card, iDEAL etc) and issuer (bank),
* and complete the payment
*
* @param Transaction $transaction
* @return RedirectResponse
* @throws \Exception
*/
public function redirectForPayment(Transaction $transaction): RedirectResponse
{
$transaction->status = TransactionStatus::PAYMENT_PENDING;
$transaction->save();
$transaction->order->status = OrderStatus::PENDING;
$transaction->order->save();
if ($transaction->payment_link == '') {
throw new \Exception('Could not redirect user to payment page. No payment page link given from psp');
}
return response()->redirectTo($transaction->payment_link, 303); //303 forces a GET redirect according to MultiSafepay.
}
/**
* Converts one of the MultiSafepay payment statuses to kms transaction statuses
*
* @param $status
* @return string the Kms shop transaction status
* @see TransactionStatus
* @see transactions.php translation file status array.
* @throws \Exception
*/
public function adaptPSPTransactionStatusToKmsTransactionStatus($status): string
{
switch ($status) {
case MultiSafepayOrderStatus::VOID:
case MultiSafepayOrderStatus::CANCELED:
return TransactionStatus::CANCELED_CUSTOMER;
case MultiSafepayOrderStatus::REFUNDED:
return TransactionStatus::REFUNDED;
case MultiSafepayOrderStatus::EXPIRED:
return TransactionStatus::EXPIRED;
case MultiSafepayOrderStatus::CHARGEDBACK:
return TransactionStatus::CHARGEDBACK;
case MultiSafepayOrderStatus::COMPLETED:
return TransactionStatus::PAYMENT_PAID;
case MultiSafepayOrderStatus::DECLINED:
return TransactionStatus::AUTHORISATION_REJECTED;
case MultiSafepayOrderStatus::INITIALIZED:
return TransactionStatus::OPEN;
case MultiSafepayOrderStatus::PARTIAL_REFUNDED:
return TransactionStatus::REFUND_PENDING;
case MultiSafepayOrderStatus::RESERVED: //Means both payment pending or refund pending
return TransactionStatus::PAYMENT_PENDING;
case MultiSafepayOrderStatus::SHIPPED:
return TransactionStatus::PAYMENT_PAID;
case MultiSafepayOrderStatus::UNCLEARED:
return TransactionStatus::PAYMENT_UNKNOWN;
}
throw new \Exception('MultiSafepay: Received an unknown payment status from MultiSafepay for Kms: '.$status);
}
/**
* Converts a payment status from the kms shop to a payment status that the psp understands
*
* @param $status
* @return mixed
* @throws \Exception
*/
public function adaptKmsTransactionStatusToPSPTransactionStatus(string $status)
{
switch ($status) {
case TransactionStatus::CANCELED:
case TransactionStatus::CANCELED_CUSTOMER:
case TransactionStatus::CANCEL_PENDING:
return MultiSafepayOrderStatus::CANCELED;
case TransactionStatus::REFUNDED:
case TransactionStatus::REFUND_PENDING:
return MultiSafepayOrderStatus::REFUNDED;
case TransactionStatus::EXPIRED:
return MultiSafepayOrderStatus::EXPIRED;
case TransactionStatus::CHARGEDBACK:
case TransactionStatus::CHARGEBACK_PENDING:
return MultiSafepayOrderStatus::CHARGEDBACK;
case TransactionStatus::PAYMENT_PAID:
return MultiSafepayOrderStatus::COMPLETED;
case TransactionStatus::AUTHORISATION_REJECTED:
return MultiSafepayOrderStatus::DECLINED;
case TransactionStatus::OPEN:
case TransactionStatus::REDIRECTED_USER_TO_PSP:
return MultiSafepayOrderStatus::INITIALIZED;
case TransactionStatus::PAYMENT_PENDING:
case TransactionStatus::AUTHORISATION_PENDING:
return MultiSafepayOrderStatus::RESERVED; //Means both payment pending or refund pending
case TransactionStatus::PAYMENT_UNKNOWN:
case TransactionStatus::AUTHORISATION_UNKNOWN:
return MultiSafepayOrderStatus::UNCLEARED;
}
throw new \Exception('MultiSafepay: Received an unknown payment status from KMS for MultiSafepay: '.$status);
}
}