Source of file PaymentService.php
Size: 19,127 Bytes - Last Modified: 2021-12-23T10:34:19+00:00
/var/www/docs.ssmods.com/process/src/src/Service/PaymentService.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 | <?php namespace SilverStripe\Omnipay\Service; use Omnipay\Common\AbstractGateway; use Omnipay\Common\CreditCard; use Omnipay\Common\Exception\OmnipayException; use Omnipay\Common\GatewayFactory; use Omnipay\Common\GatewayInterface; use Omnipay\Common\Message\AbstractRequest; use Omnipay\Common\Message\AbstractResponse; use Omnipay\Common\Message\NotificationInterface; use Omnipay\Common\Message\ResponseInterface; use Psr\Log\LoggerInterface; use SilverStripe\Control\Controller; use SilverStripe\Core\Extensible; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; use SilverStripe\Omnipay\Exception\InvalidConfigurationException; use SilverStripe\Omnipay\Exception\InvalidStateException; use SilverStripe\Omnipay\GatewayInfo; use SilverStripe\Omnipay\Helper\ErrorHandling; use SilverStripe\Omnipay\Helper\Logging; use SilverStripe\Omnipay\Model\Message\GatewayErrorMessage; use SilverStripe\Omnipay\Model\Message\NotificationError; use SilverStripe\Omnipay\Model\Message\NotificationPending; use SilverStripe\Omnipay\Model\Message\NotificationSuccessful; use SilverStripe\Omnipay\Model\Message\PaymentMessage; use SilverStripe\Omnipay\Model\Payment; use SilverStripe\Omnipay\PaymentGatewayController; /** * Provides wrapper methods for interacting with the omnipay gateways library. * * Interfaces with the omnipay library. * @property LoggerInterface $logger * @property LoggerInterface $exceptionLogger */ abstract class PaymentService { use Extensible; use Injectable; /** * */ private static $dependencies = [ 'logger' => '%$SilverStripe\Omnipay\Logger', 'exceptionLogger' => '%$SilverStripe\Omnipay\ExceptionLogger', ]; /** * @var Payment */ protected $payment; /** * @var AbstractResponse */ protected $response; /** * @var GatewayFactory */ protected $gatewayFactory; /** * @param Payment $payment the payment instance */ public function __construct(Payment $payment) { $this->payment = $payment; } /** * Initiate a gateway request with some user/application supplied data. * @param array $data payment data * @throws InvalidStateException when the payment is in a state that prevents running `complete` * @throws InvalidConfigurationException when there's a misconfiguration in the module itself * @return ServiceResponse the service response */ abstract public function initiate($data = array()); /** * Complete a previously initiated gateway request. * This is separate from initiate, since some requests require more than one step. Eg. offsite payments or * payments to gateways that return asynchronous responses. * @param array $data payment data * @param bool $isNotification whether or not this was called from a notification callback (async). Defaults to false * @throws InvalidStateException when the payment is in a state that prevents running `complete` * @throws InvalidConfigurationException when there's a misconfiguration in the module itself * @return ServiceResponse the service response */ abstract public function complete($data = array(), $isNotification = false); /** * Cancel a payment * * @throws \Exception * @throws \SilverStripe\ORM\ValidationException * @throws \SilverStripe\Omnipay\Exception\ServiceException * @return ServiceResponse */ public function cancel() { if (!$this->payment->IsComplete()) { $this->payment->Status = 'Void'; $this->payment->write(); ErrorHandling::safeExtend($this->payment, 'onCancelled'); } return $this->generateServiceResponse(ServiceResponse::SERVICE_CANCELLED); } /** * Get the payment associated with this service. * * @return Payment */ public function getPayment() { return $this->payment; } /** * Get the omnipay gateway associated with this payment, * with configuration applied. * * @throws \RuntimeException - when gateway doesn't exist. * @return GatewayInterface|AbstractGateway omnipay gateway class */ public function oGateway() { $gatewayName = $this->payment->Gateway; $gateway = $this->getGatewayFactory()->create($gatewayName); $parameters = GatewayInfo::getParameters($gatewayName); if (is_array($parameters)) { $gateway->initialize($parameters); } return $gateway; } /** * Handle a notification via gateway->acceptNotification. * * This just invokes `acceptNotification` on the gateway (if available) and wraps the return value in * the proper ServiceResponse. * * @throws InvalidConfigurationException * @throws \Omnipay\Common\Exception\InvalidRequestException * @throws \SilverStripe\Omnipay\Exception\ServiceException * @return ServiceResponse */ public function handleNotification() { $gateway = $this->oGateway(); if (!$gateway->supportsAcceptNotification()) { throw new InvalidConfigurationException( sprintf('The gateway "%s" doesn\'t support "acceptNotification"', $this->payment->Gateway) ); } // Deal with the notification, according to the omnipay documentation // https://github.com/thephpleague/omnipay#incoming-notifications $notification = null; try { $notification = $gateway->acceptNotification(); } catch (OmnipayException $e) { $this->createMessage(NotificationError::class, $e); return $this->generateServiceResponse( ServiceResponse::SERVICE_NOTIFICATION | ServiceResponse::SERVICE_ERROR ); } if (!($notification instanceof NotificationInterface)) { $this->createMessage( NotificationError::class, 'Notification from Omnipay doesn\'t implement NotificationInterface' ); return $this->generateServiceResponse( ServiceResponse::SERVICE_NOTIFICATION | ServiceResponse::SERVICE_ERROR ); } switch ($notification->getTransactionStatus()) { case NotificationInterface::STATUS_COMPLETED: $this->createMessage(NotificationSuccessful::class, $notification); return $this->generateServiceResponse(ServiceResponse::SERVICE_NOTIFICATION, $notification); break; case NotificationInterface::STATUS_PENDING: $this->createMessage(NotificationPending::class, $notification); return $this->generateServiceResponse( ServiceResponse::SERVICE_NOTIFICATION | ServiceResponse::SERVICE_PENDING, $notification ); } // The only status left is error $this->createMessage(NotificationError::class, $notification); return $this->generateServiceResponse( ServiceResponse::SERVICE_NOTIFICATION | ServiceResponse::SERVICE_ERROR, $notification ); } /** * Collect common data parameters to pass to the gateway. * This method should merge in common data that is required by all services. * * If you override this method, make sure to merge your data with parent::gatherGatewayData * * @param array $data incoming data for the gateway * @param boolean $includeCardOrToken whether or not to include card or token data * @return array */ protected function gatherGatewayData($data = array(), $includeCardOrToken = true) { //set the client IP address, if not already set if (!isset($data['clientIp'])) { $data['clientIp'] = Controller::curr()->getRequest()->getIP(); } $gatewaydata = array_merge($data, array( 'amount' => (float)$this->payment->MoneyAmount, 'currency' => $this->payment->MoneyCurrency, //set all gateway return/cancel/notify urls to PaymentGatewayController endpoint 'returnUrl' => $this->getEndpointUrl("complete"), 'cancelUrl' => $this->getEndpointUrl("cancel"), 'notifyUrl' => $this->getEndpointUrl("notify") )); // Often, the shop will want to pass in a transaction ID (order #, etc), but if there's // not one we need to set it as Ominpay requires this. if (!isset($gatewaydata['transactionId'])) { $gatewaydata['transactionId'] = $this->payment->Identifier; } if ($includeCardOrToken) { // We only look for a card if we aren't already provided with a token // Increasingly we can expect tokens or nonce's to be more common (e.g. Stripe and Braintree) $tokenKey = GatewayInfo::getTokenKey($this->payment->Gateway); if (empty($gatewaydata[$tokenKey])) { $gatewaydata['card'] = $this->getCreditCard($data); } elseif ($tokenKey !== 'token') { // some gateways (eg. braintree) use a different key but we need // to normalize that for omnipay $gatewaydata['token'] = $gatewaydata[$tokenKey]; unset($gatewaydata[$tokenKey]); } } return $gatewaydata; } /** * Generate a return/notify url for off-site gateways (completePayment). * @param string $action the action to call on the endpoint (complete, notify or cancel) * @return string endpoint url */ protected function getEndpointUrl($action) { return PaymentGatewayController::getEndpointUrl($action, $this->payment->Identifier); } /** * Get a service response from the given Omnipay response * @param ResponseInterface $omnipayResponse * @param bool $isNotification whether or not this response is a response to a notification * @throws \SilverStripe\Omnipay\Exception\ServiceException * @return ServiceResponse */ protected function wrapOmnipayResponse(ResponseInterface $omnipayResponse, $isNotification = false) { if ($isNotification) { $flags = ServiceResponse::SERVICE_NOTIFICATION; if (!$omnipayResponse->isSuccessful()) { $flags |= ServiceResponse::SERVICE_ERROR; } return $this->generateServiceResponse($flags, $omnipayResponse); } $isAsync = GatewayInfo::shouldUseAsyncNotifications($this->payment->Gateway); $flags = $isAsync ? ServiceResponse::SERVICE_PENDING : 0; if (!$omnipayResponse->isSuccessful() && !$omnipayResponse->isRedirect() && !$isAsync) { $flags |= ServiceResponse::SERVICE_ERROR; } return $this->generateServiceResponse($flags, $omnipayResponse); } /** * Mark this payment process as completed. * This sets the desired end-status on the payment, sets the transaction reference and writes the payment. * * In subclasses, you'll want to override this and: * * Log/Write the GatewayMessage * * Call a "complete" hook * * Don't forget to call the parent method from your subclass! * * @param string $endStatus the end state to set on the payment * @param ServiceResponse $serviceResponse the service response * @param mixed $gatewayMessage the message from Omnipay * @throws \SilverStripe\ORM\ValidationException */ protected function markCompleted($endStatus, ServiceResponse $serviceResponse, $gatewayMessage) { $this->payment->Status = $endStatus; if ($gatewayMessage && ($reference = $gatewayMessage->getTransactionReference())) { $this->payment->TransactionReference = $reference; } $this->payment->write(); } /** * Create a partial payment that will be based on the current payment. * This new payment will inherit the Gateway, TransactionReference, SuccessUrl and FailureUrl * of the initial payment. * @param float $amount the amount that the partial payment should have * @param string $status the desired payment status * @param boolean $write whether or not to directly write the new Payment to DB (optional) * @throws \Exception * @return Payment the newly created payment (already written to the DB) */ protected function createPartialPayment($amount, $status, $write = true) { /** @var Payment $payment */ $payment = Payment::create(array( 'Gateway' => $this->payment->Gateway, 'TransactionReference' => $this->payment->TransactionReference, 'SuccessUrl' => $this->payment->SuccessUrl, 'FailureUrl' => $this->payment->FailureUrl, 'InitialPaymentID' => $this->payment->ID )); $payment->setCurrency($this->payment->getCurrency()); $payment->setAmount($amount); // set status later, because otherwise amount and currency become immutable $payment->Status = $status; // allow extensions to update/modify the partial payment ErrorHandling::safeExtend($this, 'updatePartialPayment', $payment, $this->payment); if ($write) { ErrorHandling::safeguard(function () use (&$payment) { $payment->write(); }, 'Unable to write newly created partial Payment!'); } return $payment; } /** * Generate a service response * @param int $flags a combination of service flags * @param AbstractResponse|NotificationInterface|null $omnipayData the response or notification from the Omnipay gateway * @throws \SilverStripe\Omnipay\Exception\ServiceException * @return ServiceResponse */ protected function generateServiceResponse( $flags, $omnipayData = null ) { $response = new ServiceResponse($this->payment, $flags); if ($omnipayData) { $response->setOmnipayResponse($omnipayData); } // redirects and notifications don't need a target URL. if (!$response->isNotification() && !$response->isRedirect()) { $response->setTargetUrl( ($response->isError() || $response->isCancelled()) ? $this->payment->FailureUrl : $this->payment->SuccessUrl ); } // Hook to update service response via extensions. This can be used to customize the service response ErrorHandling::safeExtend($this, 'updateServiceResponse', $response); return $response; } /** * Record a transaction on this for this payment. * * @param string $type the type of transaction to create. * This is any class that is (or extends) PaymentMessage. * * @param array|string|AbstractResponse|AbstractRequest|OmnipayException|NotificationInterface $data * * @throws \Omnipay\Common\Exception\InvalidRequestException * @return PaymentMessage newly created DataObject, saved to database. */ protected function createMessage($type, $data = null) { $output = array(); if (is_string($data)) { $output = [ 'Message' => $data ]; } elseif (is_array($data)) { $output = $data; } elseif ($data instanceof \Exception) { $output = [ 'Message' => $data->getMessage(), 'Code' => $data->getCode(), 'Exception' => get_class($data), 'Backtrace' => $data->getTraceAsString() ]; } elseif ($data instanceof AbstractResponse) { $output = [ 'Message' => $data->getMessage(), 'Code' => $data->getCode(), 'Reference' => $data->getTransactionReference(), 'Data' => $data->getData() ]; } elseif ($data instanceof AbstractRequest) { $output = [ 'Token' => $data->getToken(), 'CardReference' => $data->getCardReference(), 'Amount' => $data->getAmount(), 'Currency' => $data->getCurrency(), 'Description' => $data->getDescription(), 'TransactionId' => $data->getTransactionId(), 'Reference' => $data->getTransactionReference(), 'ClientIp' => $data->getClientIp(), 'ReturnUrl' => $data->getReturnUrl(), 'CancelUrl' => $data->getCancelUrl(), 'NotifyUrl' => $data->getNotifyUrl(), 'Parameters' => $data->getParameters() ]; } elseif ($data instanceof NotificationInterface) { $output = [ 'Message' => $data->getMessage(), 'Code' => $data->getTransactionStatus(), 'Reference' => $data->getTransactionReference(), 'Data' => $data->getData() ]; } $output = array_merge($output, [ 'PaymentID' => $this->payment->ID, 'Gateway' => $this->payment->Gateway ]); if ($data instanceof \Exception) { $this->exceptionLogger->error($data->getMessage(), ['exception' => $data]); } else { $this->logToFile($output, $type); } /** @var PaymentMessage $message */ $message = Injector::inst()->create($type)->update($output); $message->write(); $this->payment->Messages()->add($message); return $message; } /** * Helper function for logging gateway requests * @param mixed $data Data to log. * @param string $type Error message class. */ protected function logToFile($data, $type = '') { $this->logger->log( // Log as error if we get a GatewayErrorMessage is_subclass_of($type, GatewayErrorMessage::class) ? 'error' : 'info', // Log title sprintf('%s (%s)', $type, $this->payment->Gateway), // Log context (just output the data) Logging::prepareForLogging($data) ); } /** * @throws \Psr\Container\NotFoundExceptionInterface * @return GatewayFactory */ public function getGatewayFactory() { if (!isset($this->gatewayFactory)) { $this->gatewayFactory = Injector::inst()->get('Omnipay\Common\GatewayFactory'); } return $this->gatewayFactory; } /** * @param GatewayFactory $gatewayFactory * * @return $this */ public function setGatewayFactory($gatewayFactory) { $this->gatewayFactory = $gatewayFactory; return $this; } /** * @param array $data Credit card initial parameters. * @return \Omnipay\Common\CreditCard */ protected function getCreditCard($data) { return new CreditCard($data); } } |