'payment', 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => 1, 'class' => $this->class, ]); } /* Payment steps */ public function processStep_1() { $returnUrl = createScriptURL([ 's' => 'payment', 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => 5, 'class' => $this->class, 'absolute' => true, ]); $webhookUrl = createScriptURL([ 's' => 'payment', 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => 10, 'class' => $this->class, 'absolute' => true, ]); $method = $this->determinePaymentMethod(); $methodSpecificConfig = $this->getMethodSpecificConfig($method); $amount = roundPrice($this->order->getRemainingPayment())->asFloat(); $amountInCents = roundPrice($this->order->getRemainingPayment())->mul(DecimalConstants::hundred())->asInteger(); $paymentIDSuffix = uniqid('', true); $this->createPayment( null, $amount, ['paymentClass' => static::class, 'method' => $method, 'IDSuffix' => $paymentIDSuffix] ); // save session (use our identifier that we send as merchantTxId) $session = $this->paymentId.'-'.$paymentIDSuffix; $this->updateCustomPaymentData('session', $session); // create request of selected type $request = new GiroCheckout_SDK_Request($method); $request->setSecret($methodSpecificConfig['projectPassphrase']); // String $request->addParam('merchantId', $methodSpecificConfig['merchantID']) // Integer merchant ID of a giropay project ->addParam('projectId', $methodSpecificConfig['projectID']) // integer project ID of a giropay project ->addParam('merchantTxId', $session) // String(255) unique transaction id of the merchant ->addParam('amount', $amountInCents) // Integer if a decimal currency is used, the amount has to be in the smallest unit of value, eg. Cent, Penny ->addParam('currency', 'EUR') // String(3) ->addParam('purpose', 'Bestellung '.$this->orderId) // String(27) // bic - This parameter must not be used anymore. All giropay transactions now use an external bank selection form! ->addParam('urlRedirect', $returnUrl) // URL, where the buyer has to be sent after payment ->addParam('urlNotify', $webhookUrl); // URL, where the notification has to be sent after payment $request = $this->addRequestParams($request); // the hash field is auto generated by the SDK $request->submit(); if ($request->requestHasSucceeded()) { // save gcReference (giropay request param "reference") $this->updateCustomPaymentData( 'gcReference', $request->getResponseParam('reference') ); $request->redirectCustomerToPaymentProvider(); } else { // if the transaction did not succeed, update your local system, get the responsecode and notify the customer throw new Exception($request->getResponseMessage($request->getResponseParam('rc'), 'DE')); } } protected function addRequestParams(GiroCheckout_SDK_Request $request): GiroCheckout_SDK_Request { return $request; } /** * @param string $paymentIdentifier kupshopPaymentID-uniqueSuffix */ private function getPaymentByUniqueIdentifier(string $paymentIdentifier, bool $ignoreStatus = false): ?array { foreach ($this->order->getPaymentsArray() as $payment) { $paymentData = json_decode($payment['payment_data']); if (($payment['status'] == static::STATUS_CREATED || $payment['status'] == static::STATUS_PENDING || $ignoreStatus) && isset($paymentData->paymentClass) && $paymentData->paymentClass === static::class && $payment['id'] == explode('-', $paymentIdentifier)[0] && $paymentData->IDSuffix == (explode('-', $paymentIdentifier)[1] ?? null) ) { $payment['decoded_data'] = $paymentData; return $payment; } } return null; } protected function updateCustomPaymentData($keyName, $value) { $paymentRow = sqlQueryBuilder()->select('*')->from('order_payments') ->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute()->fetch(); $data = json_decode($paymentRow['payment_data'] ?? '{}'); $data->$keyName = $value; sqlQueryBuilder()->update('order_payments') ->directValues(['payment_data' => json_encode($data)]) ->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute(); } public function processStep_5() { $method = $this->determinePaymentMethod(); $methodSpecificConfig = $this->getMethodSpecificConfig($method); $notify = new GiroCheckout_SDK_Notify($method); $notify->setSecret($methodSpecificConfig['projectPassphrase']); $notify->parseNotification($_GET); $payment = $this->getPaymentByUniqueIdentifier($notify->getResponseParam('gcMerchantTxId'), true); if (is_null($payment) || $payment['status'] == static::STATUS_STORNO) { $this->step(-3, 'storno'); } elseif ($payment['status'] == static::STATUS_FINISHED) { $this->success(translate('paymentSuccess', 'payment')); } $this->paymentId = $payment['id']; $this->saveTransactionData($notify, $payment); if ($notify->paymentSuccessful()) { // change payment status to finished if (!$this->setStatus(Payment::STATUS_FINISHED, $payment['decoded_data']->session)) { throw new Exception('GiroCheckout::setStatus failed!'); } $this->success(translate('paymentSuccess', 'payment')); } else { if (!$this->setStatus(Payment::STATUS_STORNO, $payment['decoded_data']->session)) { throw new Exception('GiroCheckout::setStatus failed!'); } $this->step(-3, 'storno'); } } /** * GiroPurchase webhook handler - check for approved payment. */ public function processStep_10() { $this->setIsNotification(true); $method = $this->determinePaymentMethod(); $methodSpecificConfig = $this->getMethodSpecificConfig($method); $notify = new GiroCheckout_SDK_Notify($method); $notify->setSecret($methodSpecificConfig['projectPassphrase']); $notify->parseNotification($_GET); $payment = $this->getPaymentByUniqueIdentifier($notify->getResponseParam('gcMerchantTxId')); if (is_null($payment)) { // send #400: The merchant did not process the notification and does not wish to be notified again. $notify->sendBadRequestStatus(); exit; } $this->paymentId = $payment['id']; $this->saveTransactionData($notify, $payment); if ($notify->paymentSuccessful()) { // change payment status to finished if (!$this->setStatus(Payment::STATUS_FINISHED, $payment['decoded_data']->session)) { throw new Exception('GiroCheckout::setStatus failed!'); } // send #200: The notification was processed correctly. $notify->sendOkStatus(); exit; } else { if (!$this->setStatus(Payment::STATUS_STORNO, $payment['decoded_data']->session)) { throw new Exception('GiroCheckout::setStatus failed!'); } // send #200: The notification was processed correctly. $notify->sendOkStatus(); exit; } } // public function getAvailableMethods(): array // { // $availableMethods = []; // foreach ($this->config['methods'] as $requestType => $methodConfig) { // $availableMethods[$requestType] = ['name' => $methodConfig['name'] ?? $requestType]; // } // // return $availableMethods; // } /** * determine payment method (request type) with fallback to first available method. */ private function determinePaymentMethod(): string { // $method = $this->order->getDataAll()['payment_data']['method'] ?? null; // $methods = $this->getAvailableMethods(); // reset($methods); // // return isset($methods[$method]) ? $method : key($methods); return $this->methodCode; } private function getMethodSpecificConfig(?string $method = null): array { // return $this->config['methods'][$method ?? $this->determinePaymentMethod()]; return $this->config; } private function saveTransactionData(GiroCheckout_SDK_Notify $notify, array $payment): void { // save transaction reference ID and result code $json = $payment['decoded_data']; $json->resultCode = $notify->getResponseParam('gcResultPayment'); // http://api.girocheckout.de/en:girocheckout:resultcodes $this->updateSQL('order_payments', ['payment_data' => json_encode($json)], ['id' => $payment['id']]); } public function hasOnlinePayment() { return true; } public static function isEnabled($className) { $cfg = Config::get(); if (empty($cfg['Modules']['payments'][$className])) { return false; } return true; } }