From 89e038d25d5a5c02dfcc5802b92218e19708dc54 Mon Sep 17 00:00:00 2001 From: mustapayev Date: Sat, 28 Dec 2024 16:17:15 +0100 Subject: [PATCH] param integration initial --- composer.json | 2 +- config/pos_test.php | 7 + examples/_templates/_header.php | 3 + examples/parampos/3d-host/_config.php | 22 + examples/parampos/3d-host/index.php | 3 + examples/parampos/3d-host/response.php | 3 + examples/parampos/3d-pay/_config.php | 22 + examples/parampos/3d-pay/form.php | 3 + examples/parampos/3d-pay/index.php | 3 + examples/parampos/3d-pay/response.php | 3 + examples/parampos/3d/_config.php | 22 + examples/parampos/3d/form.php | 3 + examples/parampos/3d/index.php | 3 + examples/parampos/3d/response.php | 3 + examples/parampos/_payment_config.php | 17 + examples/parampos/index.php | 6 + examples/parampos/regular/_config.php | 20 + examples/parampos/regular/cancel.php | 3 + examples/parampos/regular/custom_query.php | 27 + examples/parampos/regular/form.php | 3 + examples/parampos/regular/history.php | 3 + examples/parampos/regular/index.php | 3 + examples/parampos/regular/order_history.php | 3 + examples/parampos/regular/post-auth.php | 3 + examples/parampos/regular/refund.php | 3 + src/Crypt/ParamPosCrypt.php | 84 +++ .../ParamPosRequestDataMapper.php | 517 +++++++++++++++ .../ParamPosResponseDataMapper.php | 621 ++++++++++++++++++ src/Entity/Account/ParamPosAccount.php | 29 + src/Factory/AccountFactory.php | 15 + src/Factory/CryptFactory.php | 3 + src/Factory/RequestDataMapperFactory.php | 3 + src/Factory/ResponseDataMapperFactory.php | 3 + src/Factory/SerializerFactory.php | 2 + src/Gateways/ParamPos.php | 409 ++++++++++++ src/Serializer/ParamPosSerializer.php | 46 ++ 36 files changed, 1924 insertions(+), 1 deletion(-) create mode 100644 examples/parampos/3d-host/_config.php create mode 100644 examples/parampos/3d-host/index.php create mode 100644 examples/parampos/3d-host/response.php create mode 100644 examples/parampos/3d-pay/_config.php create mode 100644 examples/parampos/3d-pay/form.php create mode 100644 examples/parampos/3d-pay/index.php create mode 100644 examples/parampos/3d-pay/response.php create mode 100644 examples/parampos/3d/_config.php create mode 100644 examples/parampos/3d/form.php create mode 100644 examples/parampos/3d/index.php create mode 100644 examples/parampos/3d/response.php create mode 100644 examples/parampos/_payment_config.php create mode 100644 examples/parampos/index.php create mode 100644 examples/parampos/regular/_config.php create mode 100644 examples/parampos/regular/cancel.php create mode 100644 examples/parampos/regular/custom_query.php create mode 100644 examples/parampos/regular/form.php create mode 100644 examples/parampos/regular/history.php create mode 100644 examples/parampos/regular/index.php create mode 100644 examples/parampos/regular/order_history.php create mode 100644 examples/parampos/regular/post-auth.php create mode 100644 examples/parampos/regular/refund.php create mode 100644 src/Crypt/ParamPosCrypt.php create mode 100644 src/DataMapper/RequestDataMapper/ParamPosRequestDataMapper.php create mode 100644 src/DataMapper/ResponseDataMapper/ParamPosResponseDataMapper.php create mode 100644 src/Entity/Account/ParamPosAccount.php create mode 100644 src/Gateways/ParamPos.php create mode 100644 src/Serializer/ParamPosSerializer.php diff --git a/composer.json b/composer.json index 7bd939a6..60173ffd 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "symfony/var-dumper": "^5.1" }, "suggest": { - "ext-soap": "KuveytPos ile iptal/iade gibi ödeme olmayan işlemleri yapacaksanız." + "ext-soap": "ParamPos kullanacaksanız veya KuveytPos ile iptal/iade gibi ödeme olmayan işlemleri yapacaksanız." }, "config": { "sort-packages": true, diff --git a/config/pos_test.php b/config/pos_test.php index 6b8975e3..4b388e80 100644 --- a/config/pos_test.php +++ b/config/pos_test.php @@ -11,6 +11,13 @@ 'gateway_3d_host' => 'https://virtualpospaymentgatewaypre.akbank.com/payhosting', ], ], + 'param-pos' => [ + 'name' => 'TURK Elektronik Para A.Ş', + 'class' => Mews\Pos\Gateways\ParamPos::class, + 'gateway_endpoints' => [ + 'payment_api' => 'https://testposws.param.com.tr/turkpos.ws/service_turkpos_prod.asmx', + ], + ], 'payten_v3_hash' => [ 'name' => 'AKBANK T.A.S.', 'class' => Mews\Pos\Gateways\EstV3Pos::class, diff --git a/examples/_templates/_header.php b/examples/_templates/_header.php index cd5fb980..a467a4dc 100644 --- a/examples/_templates/_header.php +++ b/examples/_templates/_header.php @@ -28,6 +28,9 @@ + diff --git a/examples/parampos/3d-host/_config.php b/examples/parampos/3d-host/_config.php new file mode 100644 index 00000000..04469014 --- /dev/null +++ b/examples/parampos/3d-host/_config.php @@ -0,0 +1,22 @@ +get('tx', PosInterface::TX_TYPE_PAY_AUTH); + +$templateTitle = '3D Pay Model Payment'; +$paymentModel = PosInterface::MODEL_3D_PAY; diff --git a/examples/parampos/3d-pay/form.php b/examples/parampos/3d-pay/form.php new file mode 100644 index 00000000..361d8083 --- /dev/null +++ b/examples/parampos/3d-pay/form.php @@ -0,0 +1,3 @@ +get('tx', PosInterface::TX_TYPE_PAY_AUTH); + +$templateTitle = '3D Model Payment'; +$paymentModel = PosInterface::MODEL_3D_SECURE; diff --git a/examples/parampos/3d/form.php b/examples/parampos/3d/form.php new file mode 100644 index 00000000..361d8083 --- /dev/null +++ b/examples/parampos/3d/form.php @@ -0,0 +1,3 @@ + [ + // OTP 123456 + 'number' => '4446763125813623', + 'year' => '26', + 'month' => '12', + 'cvv' => '000', + 'name' => 'John Doe', + ], +]; diff --git a/examples/parampos/index.php b/examples/parampos/index.php new file mode 100644 index 00000000..8ef588d0 --- /dev/null +++ b/examples/parampos/index.php @@ -0,0 +1,6 @@ + '1020', + 'order' => [ + 'orderTrackId' => 'ae15a6c8-467e-45de-b24c-b98821a42667', + ], + 'payByLink' => [ + 'linkTxnCode' => '3000', + 'linkTransferType' => 'SMS', + 'mobilePhoneNumber' => '5321234567', + ], + 'transaction' => [ + 'amount' => 1.00, + 'currencyCode' => 949, + 'motoInd' => 0, + 'installCount' => 1, + ], + ], + null, + ]; +} diff --git a/examples/parampos/regular/form.php b/examples/parampos/regular/form.php new file mode 100644 index 00000000..2d74d020 --- /dev/null +++ b/examples/parampos/regular/form.php @@ -0,0 +1,3 @@ +getStoreKey(), + ]; + + $hashStr = \implode(static::HASH_SEPARATOR, $hashData); + + return $this->hashString($hashStr); + } + + /** + * {@inheritdoc} + */ + public function check3DHash(AbstractPosAccount $posAccount, array $data): bool + { + if (null === $posAccount->getStoreKey()) { + throw new \LogicException('Account storeKey eksik!'); + } + + $actualHash = $this->hashFromParams($posAccount->getStoreKey(), $data, 'HASHPARAMS', ':'); + + if ($data['HASH'] === $actualHash) { + $this->logger->debug('hash check is successful'); + + return true; + } + + $this->logger->error('hash check failed', [ + 'data' => $data, + 'generated_hash' => $actualHash, + 'expected_hash' => $data['HASH'], + ]); + + return false; + } + + /** + * @inheritDoc + */ + public function createHash(AbstractPosAccount $posAccount, array $requestData): string + { + $map = [ + $requestData['G']['CLIENT_CODE'], + $requestData['GUID'], + $requestData['Taksit'] ?? '', + $requestData['Islem_Tutar'], + $requestData['Toplam_Tutar'], + $requestData['Siparis_ID'], + $requestData['Hata_URL'] ?? '', + $requestData['Basarili_URL'] ?? '', + ]; + + $hashStr = \implode(static::HASH_SEPARATOR, $map); + $hashStr = mb_convert_encoding($hashStr, 'ISO-8859-9'); + + return $this->hashString($hashStr, self::HASH_ALGORITHM); + } +} diff --git a/src/DataMapper/RequestDataMapper/ParamPosRequestDataMapper.php b/src/DataMapper/RequestDataMapper/ParamPosRequestDataMapper.php new file mode 100644 index 00000000..edbe1b82 --- /dev/null +++ b/src/DataMapper/RequestDataMapper/ParamPosRequestDataMapper.php @@ -0,0 +1,517 @@ + 'TP_WMD_UCD', + PosInterface::TX_TYPE_PAY_PRE_AUTH => 'TP_Islem_Odeme_OnProv_WMD', + PosInterface::TX_TYPE_PAY_POST_AUTH => 'TP_Islem_Odeme_OnProv_Kapa', + PosInterface::TX_TYPE_CANCEL => 'TP_Islem_Iptal_Iade_Kismi2', // todo on provizyon iptal: TP_Islem_Iptal_OnProv + PosInterface::TX_TYPE_REFUND => 'TP_Islem_Iptal_Iade_Kismi2', + PosInterface::TX_TYPE_REFUND_PARTIAL => 'TP_Islem_Iptal_Iade_Kismi2', + PosInterface::TX_TYPE_STATUS => 'TP_Islem_Sorgulama4', + PosInterface::TX_TYPE_HISTORY => 'TP_Mutabakat_Ozet', // todo TP_Islem_Izleme? + ]; + + //todo + /** + * {@inheritdoc} + */ + protected array $recurringOrderFrequencyMapping = [ + 'DAY' => 'D', + 'WEEK' => 'W', + 'MONTH' => 'M', + 'YEAR' => 'Y', + ]; + + //todo + /** + * {@inheritdoc} + */ + protected array $secureTypeMappings = [ + PosInterface::MODEL_3D_SECURE => '3D', + PosInterface::MODEL_3D_PAY => '3d_pay', + PosInterface::MODEL_3D_PAY_HOSTING => '3d_pay_hosting', + PosInterface::MODEL_3D_HOST => '3d_host', + PosInterface::MODEL_NON_SECURE => 'NS', + ]; + + //todo + + /** + * {@inheritDoc} + * + * @param array{md: string, xid: string, eci: string, cavv: string} $responseData + */ + public function create3DPaymentRequestData(AbstractPosAccount $posAccount, array $order, string $txType, array $responseData): array + { + $order = $this->preparePaymentOrder($order); + + $requestData = $this->getRequestAccountData($posAccount) + [ + 'Type' => $this->mapTxType($txType), + 'IPAddress' => (string) $order['ip'], + 'OrderId' => (string) $order['id'], + 'Total' => (string) $order['amount'], + 'Currency' => $this->mapCurrency($order['currency']), + 'Taksit' => $this->mapInstallment((int) $order['installment']), + 'Number' => $responseData['md'], + 'PayerTxnId' => $responseData['xid'], + 'PayerSecurityLevel' => $responseData['eci'], + 'PayerAuthenticationCode' => $responseData['cavv'], + 'Mode' => 'P', + ]; + + if (isset($order['recurring'])) { + $requestData += $this->createRecurringData($order['recurring']); + } + + return $requestData; + } + + /** + * @param ParamPosAccount $posAccount + * @param array $order + * @param CreditCardInterface $creditCard + * + * @return array + */ + public function create3DEnrollmentCheckRequestData(AbstractPosAccount $posAccount, array $order, CreditCardInterface $creditCard): array + { + $order = $this->preparePaymentOrder($order); + + $requestData = $this->getRequestAccountData($posAccount) + [ + 'Islem_Guvenlik_Tip' => $this->secureTypeMappings[PosInterface::MODEL_3D_SECURE], //todo + 'Islem_ID' => $this->crypt->generateRandomString(), + 'IPAdr' => (string) $order['ip'], + 'Siparis_ID' => (string) $order['id'], + 'Islem_Tutar' => (string) $order['amount'], + 'Toplam_Tutar' => (string) $order['amount'], //todo + 'Basarili_URL' => (string) $order['success_url'], + 'Hata_URL' => (string) $order['fail_url'], + 'Currency' => $this->mapCurrency($order['currency']), + 'Taksit' => $this->mapInstallment((int) $order['installment']), + 'KK_Sahibi' => $creditCard->getHolderName(), + 'KK_No' => $creditCard->getNumber(), + 'KK_SK_Ay' => $creditCard->getExpirationDate(self::CREDIT_CARD_EXP_MONTH_FORMAT), + 'KK_SK_Yil' => $creditCard->getExpirationDate(self::CREDIT_CARD_EXP_YEAR_FORMAT), + 'KK_CVC' => $creditCard->getCvv(), + ]; + + $requestData['Islem_Hash'] = $this->crypt->createHash($posAccount, $requestData); +// todo +// if (isset($order['recurring'])) { +// $requestData += $this->createRecurringData($order['recurring']); +// } + + return $requestData; + } + + + //todo + + /** + * {@inheritDoc} + * @return array> + */ + public function createNonSecurePaymentRequestData(AbstractPosAccount $posAccount, array $order, string $txType, CreditCardInterface $creditCard): array + { + $order = $this->preparePaymentOrder($order); + + $requestData = $this->getRequestAccountData($posAccount) + [ + 'Islem_Guvenlik_Tip' => $this->secureTypeMappings[PosInterface::MODEL_NON_SECURE], //todo + 'Islem_ID' => $this->crypt->generateRandomString(), + 'IPAdr' => (string) $order['ip'], + 'Siparis_ID' => (string) $order['id'], + 'Islem_Tutar' => (string) $order['amount'], + 'Toplam_Tutar' => (string) $order['amount'], //todo + 'Basarili_URL' => (string) $order['success_url'], + 'Hata_URL' => (string) $order['fail_url'], + 'Currency' => $this->mapCurrency($order['currency']), + 'Taksit' => $this->mapInstallment((int) $order['installment']), + 'KK_Sahibi' => $creditCard->getHolderName(), + 'KK_No' => $creditCard->getNumber(), + 'KK_SK_Ay' => $creditCard->getExpirationDate(self::CREDIT_CARD_EXP_MONTH_FORMAT), + 'KK_SK_Yil' => $creditCard->getExpirationDate(self::CREDIT_CARD_EXP_YEAR_FORMAT), + 'KK_CVC' => $creditCard->getCvv(), + ]; + + $requestData['Islem_Hash'] = $this->crypt->createHash($posAccount, $requestData); +// todo +// if (isset($order['recurring'])) { +// $requestData += $this->createRecurringData($order['recurring']); +// } + + return $requestData; + } + + //todo + + /** + * {@inheritDoc} + * + * @return array{Type: string, OrderId: string, Name: string, Password: string, ClientId: string, Total: float|null} + */ + public function createNonSecurePostAuthPaymentRequestData(AbstractPosAccount $posAccount, array $order): array + { + $order = $this->preparePostPaymentOrder($order); + + $requestData = $this->getRequestAccountData($posAccount) + [ + 'Type' => $this->mapTxType(PosInterface::TX_TYPE_PAY_POST_AUTH), + 'OrderId' => (string) $order['id'], + 'Total' => isset($order['amount']) ? (float) $this->formatAmount($order['amount']) : null, + ]; + + if (isset($order['amount'], $order['pre_auth_amount']) && $order['pre_auth_amount'] < $order['amount']) { + // when amount < pre_auth_amount then we need to send PREAMT value + $requestData['Extra']['PREAMT'] = $order['pre_auth_amount']; + } + + return $requestData; + } + + //todo + + /** + * {@inheritDoc} + */ + public function createStatusRequestData(AbstractPosAccount $posAccount, array $order): array + { + $statusRequestData = $this->getRequestAccountData($posAccount) + [ + 'Extra' => [ + $this->mapTxType(PosInterface::TX_TYPE_STATUS) => 'QUERY', + ], + ]; + + $order = $this->prepareStatusOrder($order); + + if (isset($order['id'])) { + $statusRequestData['OrderId'] = $order['id']; + } elseif (isset($order['recurringId'])) { + $statusRequestData['Extra']['RECURRINGID'] = $order['recurringId']; + } + + return $statusRequestData; + } + + //todo + + /** + * {@inheritDoc} + */ + public function createCancelRequestData(AbstractPosAccount $posAccount, array $order): array + { + $order = $this->prepareCancelOrder($order); + + $orderData = []; + if (isset($order['recurringOrderInstallmentNumber'])) { + // this method cancels only pending recurring orders, it will not cancel already fulfilled transactions + $orderData['Extra']['RECORDTYPE'] = 'Order'; + // cancel single installment + $orderData['Extra']['RECURRINGOPERATION'] = 'Cancel'; + /** + * the order ids of recurring order installments: + * 'ORD_ID_1' => '202210121ABC', + * 'ORD_ID_2' => '202210121ABC-2', + * 'ORD_ID_3' => '202210121ABC-3', + * ... + */ + $orderData['Extra']['RECORDID'] = $order['id'].'-'.$order['recurringOrderInstallmentNumber']; + + return $this->getRequestAccountData($posAccount) + $orderData; + } + + return $this->getRequestAccountData($posAccount) + [ + 'OrderId' => $order['id'], + 'Type' => $this->mapTxType(PosInterface::TX_TYPE_CANCEL), + ]; + } + + //todo + + /** + * {@inheritDoc} + * @return array{OrderId: string, Currency: string, Type: string, Total?: string, Name: string, Password: string, ClientId: string} + */ + public function createRefundRequestData(AbstractPosAccount $posAccount, array $order, string $refundTxType): array + { + $order = $this->prepareRefundOrder($order); + + $requestData = [ + 'OrderId' => (string) $order['id'], + 'Currency' => $this->mapCurrency($order['currency']), + 'Type' => $this->mapTxType($refundTxType), + ]; + + if (isset($order['amount'])) { + $requestData['Total'] = (string) $order['amount']; + } + + return $this->getRequestAccountData($posAccount) + $requestData; + } + + //todo + + /** + * {@inheritDoc} + * @return array{OrderId: string, Extra: array&array, Name: string, Password: string, ClientId: string} + */ + public function createOrderHistoryRequestData(AbstractPosAccount $posAccount, array $order): array + { + $order = $this->prepareOrderHistoryOrder($order); + + $requestData = [ + 'OrderId' => (string) $order['id'], + 'Extra' => [ + $this->mapTxType(PosInterface::TX_TYPE_HISTORY) => 'QUERY', + ], + ]; + + return $this->getRequestAccountData($posAccount) + $requestData; + } + + //todo + + /** + * {@inheritDoc} + */ + public function createHistoryRequestData(AbstractPosAccount $posAccount, array $data = []): array + { + throw new NotImplementedException(); + } + + //todo + + /** + * {@inheritDoc} + */ + public function create3DFormData(AbstractPosAccount $posAccount, array $order, string $paymentModel, string $txType, string $gatewayURL, ?CreditCardInterface $creditCard = null): array + { + $preparedOrder = $this->preparePaymentOrder($order); + + $data = $this->create3DFormDataCommon($posAccount, $preparedOrder, $paymentModel, $txType, $gatewayURL, $creditCard); + + $event = new Before3DFormHashCalculatedEvent( + $data['inputs'], + $posAccount->getBank(), + $txType, + $paymentModel, + EstPos::class + ); + $this->eventDispatcher->dispatch($event); + $data['inputs'] = $event->getFormInputs(); + + $data['inputs']['hash'] = $this->crypt->create3DHash($posAccount, $data['inputs']); + + return $data; + } + + //todo + + /** + * @inheritDoc + */ + public function createCustomQueryRequestData(AbstractPosAccount $posAccount, array $requestData): array + { + return $requestData + $this->getRequestAccountData($posAccount); + } + + //todo + + /** + * @phpstan-param PosInterface::MODEL_3D_* $paymentModel + * @phpstan-param PosInterface::TX_TYPE_PAY_AUTH|PosInterface::TX_TYPE_PAY_PRE_AUTH $txType + * + * @param AbstractPosAccount $posAccount + * @param array $order + * @param string $paymentModel + * @param string $txType + * @param string $gatewayURL + * @param CreditCardInterface|null $creditCard + * + * @return array{gateway: string, method: 'POST', inputs: array} + * + * @throws UnsupportedTransactionTypeException + */ + protected function create3DFormDataCommon(AbstractPosAccount $posAccount, array $order, string $paymentModel, string $txType, string $gatewayURL, ?CreditCardInterface $creditCard = null): array + { + $inputs = [ + 'clientid' => $posAccount->getClientId(), + 'storetype' => $this->secureTypeMappings[$paymentModel], + 'amount' => (string) $order['amount'], + 'oid' => (string) $order['id'], + 'okUrl' => (string) $order['success_url'], + 'failUrl' => (string) $order['fail_url'], + 'rnd' => $this->crypt->generateRandomString(), + 'lang' => $this->getLang($posAccount, $order), + 'currency' => $this->mapCurrency((string) $order['currency']), + 'taksit' => $this->mapInstallment((int) $order['installment']), + 'islemtipi' => $this->mapTxType($txType), + ]; + + if ($creditCard instanceof CreditCardInterface) { + $inputs['pan'] = $creditCard->getNumber(); + $inputs['Ecom_Payment_Card_ExpDate_Month'] = $creditCard->getExpireMonth(self::CREDIT_CARD_EXP_MONTH_FORMAT); + $inputs['Ecom_Payment_Card_ExpDate_Year'] = $creditCard->getExpireYear(self::CREDIT_CARD_EXP_YEAR_FORMAT); + $inputs['cv2'] = $creditCard->getCvv(); + } + + return [ + 'gateway' => $gatewayURL, + 'method' => 'POST', + 'inputs' => $inputs, + ]; + } + + /** + * 0 => '1' + * 1 => '1' + * 2 => '2' + * @inheritDoc + */ + protected function mapInstallment(int $installment): string + { + return $installment > 1 ? (string) $installment : '1'; + } + + //todo + + /** + * @inheritDoc + */ + protected function preparePaymentOrder(array $order): array + { + return \array_merge($order, [ + 'installment' => $order['installment'] ?? 0, + 'currency' => $order['currency'] ?? PosInterface::CURRENCY_TRY, //todo doviz odeme nasil olacak? + 'amount' => $order['amount'], + 'success_url' => $order['success_url'], + 'fail_url' => $order['fail_url'], + 'ip' => $order['ip'], + ]); + } + + //todo + + /** + * @inheritDoc + */ + protected function preparePostPaymentOrder(array $order): array + { + return [ + 'id' => $order['id'], + 'amount' => $order['amount'] ?? null, + 'pre_auth_amount' => $order['pre_auth_amount'] ?? null, + ]; + } + + +//todo + + /** + * @inheritDoc + */ + protected function prepareRefundOrder(array $order): array + { + return [ + 'id' => $order['id'], + 'currency' => $order['currency'] ?? PosInterface::CURRENCY_TRY, + 'amount' => $order['amount'], + ]; + } + + //todo + + /** + * @inheritDoc + */ + protected function prepareOrderHistoryOrder(array $order): array + { + return [ + 'id' => $order['id'], + ]; + } + + //todo + + /** + * @inheritDoc + * + * @return string + */ + protected function mapCurrency(string $currency): string + { + return (string) $this->currencyMappings[$currency] ?? $currency; + } + + /** + * @param AbstractPosAccount $posAccount + * + * @return array{G: array{CLIENT_CODE: string, CLIENT_USERNAME: string, CLIENT_PASSWORD: string}} + */ + private function getRequestAccountData(AbstractPosAccount $posAccount): array + { + return [ + 'G' => [ + 'CLIENT_CODE' => $posAccount->getClientId(), + 'CLIENT_USERNAME' => $posAccount->getUsername(), + 'CLIENT_PASSWORD' => $posAccount->getPassword(), + ], + 'GUID' => $posAccount->getStoreKey(), + ]; + } + + //todo + + /** + * @param array{frequency: int, frequencyType: string, installment: int} $recurringData + * + * @return array{PbOrder: array{OrderType: string, OrderFrequencyInterval: string, OrderFrequencyCycle: string, TotalNumberPayments: string}} + */ + private function createRecurringData(array $recurringData): array + { + return [ + 'PbOrder' => [ + 'OrderType' => '0', // 0: Varsayılan, taksitsiz + // Periyodik İşlem Frekansı + 'OrderFrequencyInterval' => (string) $recurringData['frequency'], + // D|M|Y + 'OrderFrequencyCycle' => $this->mapRecurringFrequency($recurringData['frequencyType']), + 'TotalNumberPayments' => (string) $recurringData['installment'], + ], + ]; + } +} diff --git a/src/DataMapper/ResponseDataMapper/ParamPosResponseDataMapper.php b/src/DataMapper/ResponseDataMapper/ParamPosResponseDataMapper.php new file mode 100644 index 00000000..b8050af1 --- /dev/null +++ b/src/DataMapper/ResponseDataMapper/ParamPosResponseDataMapper.php @@ -0,0 +1,621 @@ + + */ + protected array $codes = [ + self::PROCEDURE_SUCCESS_CODE => self::TX_APPROVED, + ]; + + // todo + /** + * D : Başarısız işlem + * A : Otorizasyon, gün sonu kapanmadan + * C : Ön otorizasyon kapama, gün sonu kapanmadan + * PN : Bekleyen İşlem + * CNCL : İptal Edilmiş İşlem + * ERR : Hata Almış İşlem + * S : Satış + * R : Teknik İptal gerekiyor + * V : İptal + * @var array + */ + protected array $orderStatusMappings = [ + 'D' => PosInterface::PAYMENT_STATUS_ERROR, + 'ERR' => PosInterface::PAYMENT_STATUS_ERROR, + 'A' => PosInterface::PAYMENT_STATUS_PAYMENT_COMPLETED, + 'C' => PosInterface::PAYMENT_STATUS_PAYMENT_COMPLETED, + 'S' => PosInterface::PAYMENT_STATUS_PAYMENT_COMPLETED, + 'PN' => PosInterface::PAYMENT_STATUS_PAYMENT_PENDING, + 'CNCL' => PosInterface::PAYMENT_STATUS_CANCELED, + 'V' => PosInterface::PAYMENT_STATUS_CANCELED, + ]; + + // todo + /** + * @param PaymentStatusModel $rawPaymentResponseData + * {@inheritDoc} + */ + public function mapPaymentResponse(array $rawPaymentResponseData, string $txType, array $order): array + { + $this->logger->debug('mapping payment response', [$rawPaymentResponseData]); + + $defaultResponse = $this->getDefaultPaymentResponse($txType, PosInterface::MODEL_NON_SECURE); + if ([] === $rawPaymentResponseData) { + return $defaultResponse; + } + + $rawPaymentResponseData = $this->emptyStringsToNull($rawPaymentResponseData); + + $procReturnCode = $this->getProcReturnCode($rawPaymentResponseData); + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } + + $extra = $rawPaymentResponseData['Extra']; + + $mappedResponse = [ + 'order_id' => $rawPaymentResponseData['OrderId'], + 'currency' => $order['currency'], + 'amount' => $order['amount'], + 'group_id' => $rawPaymentResponseData['GroupId'], + 'transaction_id' => $rawPaymentResponseData['TransId'], + 'transaction_time' => self::TX_APPROVED === $status ? new \DateTimeImmutable($extra['TRXDATE']) : null, + 'auth_code' => $rawPaymentResponseData['AuthCode'] ?? null, + 'ref_ret_num' => $rawPaymentResponseData['HostRefNum'], + 'proc_return_code' => $procReturnCode, + 'status' => $status, + 'status_detail' => $this->getStatusDetail($procReturnCode), + 'error_code' => self::TX_APPROVED === $status ? null : $extra['ERRORCODE'], + 'error_message' => self::TX_APPROVED === $status ? null : $rawPaymentResponseData['ErrMsg'], + 'recurring_id' => $extra['RECURRINGID'] ?? null, // set when recurring payment is made + 'all' => $rawPaymentResponseData, + ]; + + $this->logger->debug('mapped payment response', $mappedResponse); + + return $this->mergeArraysPreferNonNullValues($defaultResponse, $mappedResponse); + } + + // todo + /** + * @param PaymentStatusModel|null $rawPaymentResponseData + * {@inheritdoc} + */ + public function map3DPaymentData(array $raw3DAuthResponseData, ?array $rawPaymentResponseData, string $txType, array $order): array + { + $this->logger->debug('mapping 3D payment data', [ + '3d_auth_response' => $raw3DAuthResponseData, + 'provision_response' => $rawPaymentResponseData, + ]); + $raw3DAuthResponseData = $this->emptyStringsToNull($raw3DAuthResponseData); + $paymentModel = $this->mapSecurityType($raw3DAuthResponseData['storetype']); + $paymentResponseData = $this->getDefaultPaymentResponse($txType, $paymentModel); + $mdStatus = $this->extractMdStatus($raw3DAuthResponseData); + if (null !== $rawPaymentResponseData) { + $paymentResponseData = $this->mapPaymentResponse($rawPaymentResponseData, $txType, $order); + } + + $threeDResponse = [ + 'transaction_security' => null === $mdStatus ? null : $this->mapResponseTransactionSecurity($mdStatus), + 'md_status' => $mdStatus, + 'order_id' => $raw3DAuthResponseData['oid'], + 'masked_number' => $raw3DAuthResponseData['maskedCreditCard'], + 'month' => $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Month'], + 'year' => $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Year'], + 'amount' => null !== $raw3DAuthResponseData['amount'] ? $this->formatAmount($raw3DAuthResponseData['amount']) : null, + 'currency' => '*' === $raw3DAuthResponseData['currency'] ? null : $this->mapCurrency($raw3DAuthResponseData['currency']), + 'installment_count' => $this->mapInstallment($raw3DAuthResponseData['taksit']), + 'eci' => null, + 'tx_status' => null, + 'cavv' => null, + 'md_error_message' => null, + '3d_all' => $raw3DAuthResponseData, + ]; + + if (null !== $mdStatus) { + if (!$this->is3dAuthSuccess($mdStatus)) { + $threeDResponse['md_error_message'] = $raw3DAuthResponseData['mdErrorMsg']; + } + } else { + $threeDResponse['error_code'] = $raw3DAuthResponseData['ErrorCode']; + $threeDResponse['error_message'] = $raw3DAuthResponseData['ErrMsg']; + } + + if ($this->is3dAuthSuccess($mdStatus)) { + $threeDResponse['eci'] = $raw3DAuthResponseData['eci']; + $threeDResponse['cavv'] = $raw3DAuthResponseData['cavv']; + } + + $result = $this->mergeArraysPreferNonNullValues($threeDResponse, $paymentResponseData); + $result['payment_model'] = $paymentModel; + + return $result; + } + + // todo + /** + * {@inheritdoc} + */ + public function map3DPayResponseData(array $raw3DAuthResponseData, string $txType, array $order): array + { + $status = self::TX_DECLINED; + + $raw3DAuthResponseData = $this->emptyStringsToNull($raw3DAuthResponseData); + $procReturnCode = $this->getProcReturnCode($raw3DAuthResponseData); + $mdStatus = $this->extractMdStatus($raw3DAuthResponseData); + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode && $this->is3dAuthSuccess($mdStatus)) { + $status = self::TX_APPROVED; + } + + $paymentModel = $this->mapSecurityType($raw3DAuthResponseData['storetype']); + $defaultResponse = $this->getDefaultPaymentResponse($txType, $paymentModel); + + $response = [ + 'order_id' => $raw3DAuthResponseData['oid'], + 'transaction_security' => null === $mdStatus ? null : $this->mapResponseTransactionSecurity($mdStatus), + 'md_status' => $mdStatus, + 'status' => $status, + 'proc_return_code' => $procReturnCode, + 'masked_number' => $raw3DAuthResponseData['maskedCreditCard'], + 'month' => $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Month'], + 'year' => $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Year'], + 'amount' => $this->formatAmount($raw3DAuthResponseData['amount']), + 'currency' => $this->mapCurrency($raw3DAuthResponseData['currency']), + 'installment_count' => $this->mapInstallment($raw3DAuthResponseData['taksit']), + 'tx_status' => null, + 'eci' => null, + 'cavv' => null, + 'md_error_message' => $raw3DAuthResponseData['mdErrorMsg'], + 'all' => $raw3DAuthResponseData, + ]; + + if (self::TX_APPROVED === $status) { + $response['auth_code'] = $raw3DAuthResponseData['AuthCode']; + $response['eci'] = $raw3DAuthResponseData['eci']; + $response['cavv'] = $raw3DAuthResponseData['cavv']; + $response['transaction_id'] = $raw3DAuthResponseData['TransId']; + $response['transaction_time'] = new \DateTimeImmutable($raw3DAuthResponseData['EXTRA_TRXDATE']); + $response['ref_ret_num'] = $raw3DAuthResponseData['HostRefNum']; + $response['status_detail'] = $this->getStatusDetail($procReturnCode); + $response['error_message'] = $raw3DAuthResponseData['ErrMsg']; + $response['error_code'] = isset($raw3DAuthResponseData['ErrMsg']) ? $procReturnCode : null; + } + + return $this->mergeArraysPreferNonNullValues($defaultResponse, $response); + } + + // todo + /** + * {@inheritdoc} + */ + public function map3DHostResponseData(array $raw3DAuthResponseData, string $txType, array $order): array + { + $raw3DAuthResponseData = $this->emptyStringsToNull($raw3DAuthResponseData); + $status = self::TX_DECLINED; + $mdStatus = $this->extractMdStatus($raw3DAuthResponseData); + if ($this->is3dAuthSuccess($mdStatus)) { + $status = self::TX_APPROVED; + } + + $paymentModel = $this->mapSecurityType($raw3DAuthResponseData['storetype']); + $defaultResponse = $this->getDefaultPaymentResponse($txType, $paymentModel); + + $response = [ + 'order_id' => $raw3DAuthResponseData['oid'], + 'transaction_security' => null === $mdStatus ? null : $this->mapResponseTransactionSecurity($mdStatus), + 'md_status' => $mdStatus, + 'status' => $status, + 'amount' => $this->formatAmount($raw3DAuthResponseData['amount']), + 'currency' => $this->mapCurrency($raw3DAuthResponseData['currency']), + 'installment_count' => $this->mapInstallment($raw3DAuthResponseData['taksit']), + 'tx_status' => null, + 'masked_number' => null, + 'month' => null, + 'year' => null, + 'eci' => null, + 'cavv' => null, + 'md_error_message' => self::TX_APPROVED !== $status ? $raw3DAuthResponseData['mdErrorMsg'] : null, + 'all' => $raw3DAuthResponseData, + ]; + + if (isset($raw3DAuthResponseData['maskedCreditCard'])) { + $response['masked_number'] = $raw3DAuthResponseData['maskedCreditCard']; + $response['month'] = $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Month']; + $response['year'] = $raw3DAuthResponseData['Ecom_Payment_Card_ExpDate_Year']; + if (self::TX_APPROVED === $status) { + $response['eci'] = $raw3DAuthResponseData['eci']; + $response['cavv'] = $raw3DAuthResponseData['cavv']; + $response['transaction_time'] = new \DateTimeImmutable(); + } + } + + return $this->mergeArraysPreferNonNullValues($defaultResponse, $response); + } + + // todo + /** + * @param PaymentStatusModel $rawResponseData + * {@inheritdoc} + */ + public function mapRefundResponse(array $rawResponseData): array + { + /** @var PaymentStatusModel $rawResponseData */ + $rawResponseData = $this->emptyStringsToNull($rawResponseData); + $procReturnCode = $this->getProcReturnCode($rawResponseData); + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } + + $result = [ + 'order_id' => $rawResponseData['OrderId'], + 'group_id' => null, + 'auth_code' => null, + 'ref_ret_num' => $rawResponseData['HostRefNum'], + 'proc_return_code' => $procReturnCode, + 'transaction_id' => $rawResponseData['TransId'], + 'num_code' => null, + 'error_code' => null, + 'error_message' => $rawResponseData['ErrMsg'], + 'status' => $status, + 'status_detail' => $this->getStatusDetail($procReturnCode), + 'all' => $rawResponseData, + ]; + + if (self::TX_APPROVED === $status) { + $result['group_id'] = $rawResponseData['GroupId']; + $result['auth_code'] = $rawResponseData['AuthCode']; + $result['num_code'] = $rawResponseData['Extra']['NUMCODE']; + } else { + $result['error_code'] = $rawResponseData['Extra']['ERRORCODE'] ?? $rawResponseData['ERRORCODE'] ?? null; + } + + return $result; + } + + // todo + /** + * @param PaymentStatusModel $rawResponseData + * + * {@inheritdoc} + */ + public function mapCancelResponse(array $rawResponseData): array + { + /** @var PaymentStatusModel $rawResponseData */ + $rawResponseData = $this->emptyStringsToNull($rawResponseData); + $procReturnCode = $this->getProcReturnCode($rawResponseData); + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } + + if (isset($rawResponseData['RECURRINGOPERATION'])) { + if ('Successfull' === $rawResponseData['RESULT']) { + $status = self::TX_APPROVED; + } + + return [ + 'order_id' => $rawResponseData['RECORDID'], + 'status' => $status, + 'all' => $rawResponseData, + ]; + } + + $result = [ + 'order_id' => $rawResponseData['OrderId'], + 'group_id' => null, + 'auth_code' => null, + 'ref_ret_num' => $rawResponseData['HostRefNum'], + 'proc_return_code' => $procReturnCode, + 'transaction_id' => $rawResponseData['TransId'], + 'error_code' => null, + 'num_code' => null, + 'error_message' => $rawResponseData['ErrMsg'], + 'status' => $status, + 'status_detail' => $this->getStatusDetail($procReturnCode), + 'all' => $rawResponseData, + ]; + + if (self::TX_APPROVED === $status) { + $result['group_id'] = $rawResponseData['GroupId']; + $result['auth_code'] = $rawResponseData['AuthCode']; + $result['num_code'] = $rawResponseData['Extra']['NUMCODE']; + } else { + $result['error_code'] = $rawResponseData['Extra']['ERRORCODE'] ?? $rawResponseData['ERRORCODE'] ?? null; + } + + return $result; + } + + // todo + /** + * @param PaymentStatusModel $rawResponseData + * {@inheritdoc} + */ + public function mapStatusResponse(array $rawResponseData): array + { + $rawResponseData = $this->emptyStringsToNull($rawResponseData); + $procReturnCode = $this->getProcReturnCode($rawResponseData); + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } + + $extra = $rawResponseData['Extra']; + + if (isset($extra['RECURRINGID'])) { + return $this->mapRecurringStatusResponse($rawResponseData); + } + + $defaultResponse = $this->getDefaultStatusResponse($rawResponseData); + + $defaultResponse['order_id'] = $rawResponseData['OrderId']; + $defaultResponse['proc_return_code'] = $procReturnCode; + $defaultResponse['transaction_id'] = $rawResponseData['TransId']; + $defaultResponse['error_message'] = self::TX_APPROVED === $status ? null : $rawResponseData['ErrMsg']; + $defaultResponse['status'] = $status; + $defaultResponse['status_detail'] = $this->getStatusDetail($procReturnCode); + + $result = $defaultResponse; + if (self::TX_APPROVED === $status) { + $result['auth_code'] = $extra['AUTH_CODE']; + $result['ref_ret_num'] = $extra['HOST_REF_NUM']; + $result['first_amount'] = $this->formatAmount($extra['ORIG_TRANS_AMT']); + $result['capture_amount'] = null !== $extra['CAPTURE_AMT'] ? $this->formatAmount($extra['CAPTURE_AMT']) : null; + $result['masked_number'] = $extra['PAN']; + $result['num_code'] = $extra['NUMCODE']; + $result['capture'] = $result['first_amount'] === $result['capture_amount']; + $txType = 'S' === $extra['CHARGE_TYPE_CD'] ? PosInterface::TX_TYPE_PAY_AUTH : PosInterface::TX_TYPE_REFUND; + $result['transaction_type'] = $txType; + $result['order_status'] = $this->orderStatusMappings[$extra['TRANS_STAT']] ?? null; + $result['transaction_time'] = isset($extra['AUTH_DTTM']) ? new \DateTimeImmutable($extra['AUTH_DTTM']) : null; + $result['capture_time'] = isset($extra['CAPTURE_DTTM']) ? new \DateTimeImmutable($extra['CAPTURE_DTTM']) : null; + $result['cancel_time'] = isset($extra['VOID_DTTM']) ? new \DateTimeImmutable($extra['VOID_DTTM']) : null; + } + + return $result; + } + +// todo + /** + * @param array $rawResponseData + * + * @return array + */ + public function mapRecurringStatusResponse(array $rawResponseData): array + { + $status = self::TX_DECLINED; + /** @var array $extra */ + $extra = $rawResponseData['Extra']; + if (isset($extra['RECURRINGCOUNT']) && $extra['RECURRINGCOUNT'] > 0) { + // when order not found for the given recurring order id then RECURRINGCOUNT = 0 + $status = self::TX_APPROVED; + } + + $recurringOrderResponse = [ + 'recurringId' => $extra['RECURRINGID'], + 'recurringInstallmentCount' => $extra['RECURRINGCOUNT'], + 'status' => $status, + 'num_code' => $extra['NUMCODE'], + 'error_message' => $status !== self::TX_APPROVED ? $rawResponseData['ErrMsg'] : null, + 'all' => $rawResponseData, + ]; + + for ($i = 1; isset($extra[\sprintf('ORD_ID_%d', $i)]); ++$i) { + $recurringOrderResponse['recurringOrders'][] = $this->mapSingleRecurringOrderStatus($extra, $i); + } + + return $recurringOrderResponse; + } + + // todo + /** + * @param PaymentStatusModel $rawResponseData + * + * {@inheritDoc} + */ + public function mapOrderHistoryResponse(array $rawResponseData): array + { + $rawResponseData = $this->emptyStringsToNull($rawResponseData); + $procReturnCode = $this->getProcReturnCode($rawResponseData); + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } + + $transactions = []; + $i = 1; + while (isset($rawResponseData['Extra']['TRX'.$i])) { + $rawTx = \explode("\t", $rawResponseData['Extra']['TRX'.$i]); + $transactions[] = $this->mapSingleOrderHistoryTransaction($rawTx); + ++$i; + } + + return [ + /** @var PaymentStatusModel $rawResponseData */ + 'order_id' => $rawResponseData['OrderId'], + 'proc_return_code' => $procReturnCode, + 'error_message' => $rawResponseData['ErrMsg'], + 'num_code' => $rawResponseData['Extra']['NUMCODE'], + 'trans_count' => (int) $rawResponseData['Extra']['TRXCOUNT'], + 'transactions' => \array_reverse($transactions), + 'status' => $status, + 'status_detail' => $this->getStatusDetail($procReturnCode), + 'all' => $rawResponseData, + ]; + } + + // todo + /** + * {@inheritDoc} + */ + public function mapHistoryResponse(array $rawResponseData): array + { + throw new NotImplementedException(); + } + + // todo + /** + * @inheritDoc + */ + public function is3dAuthSuccess(?string $mdStatus): bool + { + return $mdStatus === '1'; + } + + // todo + /** + * @inheritDoc + */ + public function extractMdStatus(array $raw3DAuthResponseData): ?string + { + return $raw3DAuthResponseData['mdStatus'] ?? null; + } + + // todo + /** + * @param string $mdStatus + * + * @return string + */ + protected function mapResponseTransactionSecurity(string $mdStatus): string + { + $transactionSecurity = 'MPI fallback'; + if ('1' === $mdStatus) { + $transactionSecurity = 'Full 3D Secure'; + } elseif (\in_array($mdStatus, ['2', '3', '4'])) { + $transactionSecurity = 'Half 3D Secure'; + } + + return $transactionSecurity; + } + + // todo + /** + * Get Status Detail Text + * + * @param string|null $procReturnCode + * + * @return string|null + */ + protected function getStatusDetail(?string $procReturnCode): ?string + { + return $this->codes[$procReturnCode] ?? null; + } + + // todo + /** + * Get ProcReturnCode + * + * @param array $response + * + * @return string|null + */ + protected function getProcReturnCode(array $response): ?string + { + return $response['ProcReturnCode'] ?? null; + } + + // todo + /** + * "100001" => 1000.01 odeme durum sorgulandiginda gelen amount format + * "1000.01" => 1000.01 odeme yapildiginda gelen amount format + * + * @param string $amount + * + * @return float + */ + protected function formatAmount(string $amount): float + { + return ((float) \str_replace('.', '', $amount)) / 100; + } + + // todo + /** + * @param array $rawTx + * + * @return array + */ + private function mapSingleOrderHistoryTransaction(array $rawTx): array + { + $rawTx = $this->emptyStringsToNull($rawTx); + $transaction = $this->getDefaultOrderHistoryTxResponse(); + $transaction['auth_code'] = $rawTx[8]; + $transaction['proc_return_code'] = $rawTx[9]; + if (self::PROCEDURE_SUCCESS_CODE === $transaction['proc_return_code']) { + $transaction['status'] = self::TX_APPROVED; + } + + $transaction['status_detail'] = $this->getStatusDetail($transaction['proc_return_code']); + $transaction['transaction_id'] = $rawTx[10]; + /** + * S: Auth/PreAuth/PostAuth + * C: Refund + */ + $transaction['transaction_type'] = 'S' === $rawTx[0] ? PosInterface::TX_TYPE_PAY_AUTH : PosInterface::TX_TYPE_REFUND; + $transaction['order_status'] = $this->orderStatusMappings[$rawTx[1]] ?? null; + $transaction['transaction_time'] = new \DateTimeImmutable($rawTx[4]); + $transaction['first_amount'] = null === $rawTx[2] ? null : $this->formatAmount($rawTx[2]); + $transaction['capture_amount'] = null === $rawTx[3] ? null : $this->formatAmount($rawTx[3]); + $transaction['capture'] = self::TX_APPROVED === $transaction['status'] && $transaction['first_amount'] === $transaction['capture_amount']; + $transaction['ref_ret_num'] = $rawTx[7]; + + return $transaction; + } + + /** + * @param array $extra + * @param int<1, max> $i + * + * @return array + */ + private function mapSingleRecurringOrderStatus(array $extra, int $i): array + { + $procReturnCode = $extra[\sprintf('PROC_RET_CD_%d', $i)] ?? null; + $status = self::TX_DECLINED; + if (self::PROCEDURE_SUCCESS_CODE === $procReturnCode) { + $status = self::TX_APPROVED; + } elseif (null === $procReturnCode) { + $status = null; + } + + $recurringOrder = [ + 'order_id' => $extra[\sprintf('ORD_ID_%d', $i)], + 'masked_number' => $extra[\sprintf('PAN_%d', $i)], + 'order_status' => $this->orderStatusMappings[$extra[\sprintf('TRANS_STAT_%d', $i)]] ?? null, + + // following fields are null until transaction is done for respective installment: + 'auth_code' => $extra[\sprintf('AUTH_CODE_%d', $i)] ?? null, + 'proc_return_code' => $procReturnCode, + 'transaction_type' => 'S' === $extra[\sprintf('CHARGE_TYPE_CD_%d', $i)] ? PosInterface::TX_TYPE_PAY_AUTH : PosInterface::TX_TYPE_REFUND, + 'status' => $status, + 'status_detail' => $this->getStatusDetail($procReturnCode), + 'transaction_time' => isset($extra[\sprintf('AUTH_DTTM_%d', $i)]) ? new \DateTimeImmutable($extra[\sprintf('AUTH_DTTM_%d', $i)]) : null, + 'capture_time' => isset($extra[\sprintf('CAPTURE_DTTM_%d', $i)]) ? new \DateTimeImmutable($extra[\sprintf('CAPTURE_DTTM_%d', $i)]) : null, + 'transaction_id' => $extra[\sprintf('TRANS_ID_%d', $i)] ?? null, + 'ref_ret_num' => $extra[\sprintf('HOST_REF_NUM_%d', $i)] ?? null, + 'first_amount' => isset($extra[\sprintf('ORIG_TRANS_AMT_%d', $i)]) ? $this->formatAmount($extra[\sprintf('ORIG_TRANS_AMT_%d', $i)]) : null, + 'capture_amount' => isset($extra[\sprintf('CAPTURE_AMT_%d', $i)]) ? $this->formatAmount($extra[\sprintf('CAPTURE_AMT_%d', $i)]) : null, + ]; + + + $recurringOrder['capture'] = $recurringOrder['first_amount'] === $recurringOrder['capture_amount']; + + return $this->mergeArraysPreferNonNullValues($this->getDefaultOrderHistoryTxResponse(), $recurringOrder); + } +} diff --git a/src/Entity/Account/ParamPosAccount.php b/src/Entity/Account/ParamPosAccount.php new file mode 100644 index 00000000..ea0c07dd --- /dev/null +++ b/src/Entity/Account/ParamPosAccount.php @@ -0,0 +1,29 @@ + GarantiPosCrypt::class, InterPos::class => InterPosCrypt::class, KuveytPos::class => KuveytPosCrypt::class, + ParamPos::class => ParamPosCrypt::class, PayFlexCPV4Pos::class => PayFlexCPV4Crypt::class, PayForPos::class => PayForPosCrypt::class, PosNet::class => PosNetCrypt::class, diff --git a/src/Factory/RequestDataMapperFactory.php b/src/Factory/RequestDataMapperFactory.php index e2f8ba61..6fdec6a4 100644 --- a/src/Factory/RequestDataMapperFactory.php +++ b/src/Factory/RequestDataMapperFactory.php @@ -14,6 +14,7 @@ use Mews\Pos\DataMapper\RequestDataMapper\GarantiPosRequestDataMapper; use Mews\Pos\DataMapper\RequestDataMapper\InterPosRequestDataMapper; use Mews\Pos\DataMapper\RequestDataMapper\KuveytPosRequestDataMapper; +use Mews\Pos\DataMapper\RequestDataMapper\ParamPosRequestDataMapper; use Mews\Pos\DataMapper\RequestDataMapper\PayFlexCPV4PosRequestDataMapper; use Mews\Pos\DataMapper\RequestDataMapper\PayFlexV4PosRequestDataMapper; use Mews\Pos\DataMapper\RequestDataMapper\PayForPosRequestDataMapper; @@ -28,6 +29,7 @@ use Mews\Pos\Gateways\GarantiPos; use Mews\Pos\Gateways\InterPos; use Mews\Pos\Gateways\KuveytPos; +use Mews\Pos\Gateways\ParamPos; use Mews\Pos\Gateways\PayFlexCPV4Pos; use Mews\Pos\Gateways\PayFlexV4Pos; use Mews\Pos\Gateways\PayForPos; @@ -60,6 +62,7 @@ public static function createGatewayRequestMapper(string $gatewayClass, EventDis GarantiPos::class => GarantiPosRequestDataMapper::class, InterPos::class => InterPosRequestDataMapper::class, KuveytPos::class => KuveytPosRequestDataMapper::class, + ParamPos::class => ParamPosRequestDataMapper::class, PayFlexCPV4Pos::class => PayFlexCPV4PosRequestDataMapper::class, PayFlexV4Pos::class => PayFlexV4PosRequestDataMapper::class, PayForPos::class => PayForPosRequestDataMapper::class, diff --git a/src/Factory/ResponseDataMapperFactory.php b/src/Factory/ResponseDataMapperFactory.php index a73b8788..67cb54f7 100644 --- a/src/Factory/ResponseDataMapperFactory.php +++ b/src/Factory/ResponseDataMapperFactory.php @@ -13,6 +13,7 @@ use Mews\Pos\DataMapper\ResponseDataMapper\GarantiPosResponseDataMapper; use Mews\Pos\DataMapper\ResponseDataMapper\InterPosResponseDataMapper; use Mews\Pos\DataMapper\ResponseDataMapper\KuveytPosResponseDataMapper; +use Mews\Pos\DataMapper\ResponseDataMapper\ParamPosResponseDataMapper; use Mews\Pos\DataMapper\ResponseDataMapper\PayFlexCPV4PosResponseDataMapper; use Mews\Pos\DataMapper\ResponseDataMapper\PayFlexV4PosResponseDataMapper; use Mews\Pos\DataMapper\ResponseDataMapper\PayForPosResponseDataMapper; @@ -27,6 +28,7 @@ use Mews\Pos\Gateways\GarantiPos; use Mews\Pos\Gateways\InterPos; use Mews\Pos\Gateways\KuveytPos; +use Mews\Pos\Gateways\ParamPos; use Mews\Pos\Gateways\PayFlexCPV4Pos; use Mews\Pos\Gateways\PayFlexV4Pos; use Mews\Pos\Gateways\PayForPos; @@ -57,6 +59,7 @@ public static function createGatewayResponseMapper(string $gatewayClass, Request GarantiPos::class => GarantiPosResponseDataMapper::class, InterPos::class => InterPosResponseDataMapper::class, KuveytPos::class => KuveytPosResponseDataMapper::class, + ParamPos::class => ParamPosResponseDataMapper::class, PayFlexCPV4Pos::class => PayFlexCPV4PosResponseDataMapper::class, PayFlexV4Pos::class => PayFlexV4PosResponseDataMapper::class, PayForPos::class => PayForPosResponseDataMapper::class, diff --git a/src/Factory/SerializerFactory.php b/src/Factory/SerializerFactory.php index 933aa44b..3edfe3d9 100644 --- a/src/Factory/SerializerFactory.php +++ b/src/Factory/SerializerFactory.php @@ -12,6 +12,7 @@ use Mews\Pos\Serializer\GarantiPosSerializer; use Mews\Pos\Serializer\InterPosSerializer; use Mews\Pos\Serializer\KuveytPosSerializer; +use Mews\Pos\Serializer\ParamPosSerializer; use Mews\Pos\Serializer\PayFlexCPV4PosSerializer; use Mews\Pos\Serializer\PayFlexV4PosSerializer; use Mews\Pos\Serializer\PayForPosSerializer; @@ -46,6 +47,7 @@ public static function createGatewaySerializer(string $gatewayClass): Serializer PosNetV1PosSerializer::class, ToslaPosSerializer::class, VakifKatilimPosSerializer::class, + ParamPosSerializer::class, ]; /** @var class-string $serializer */ diff --git a/src/Gateways/ParamPos.php b/src/Gateways/ParamPos.php new file mode 100644 index 00000000..24c21bec --- /dev/null +++ b/src/Gateways/ParamPos.php @@ -0,0 +1,409 @@ + [ + PosInterface::MODEL_3D_SECURE, + PosInterface::MODEL_3D_PAY, + PosInterface::MODEL_3D_HOST, + PosInterface::MODEL_NON_SECURE, + ], + PosInterface::TX_TYPE_PAY_PRE_AUTH => [ + PosInterface::MODEL_3D_PAY, + PosInterface::MODEL_3D_HOST, + ], + + PosInterface::TX_TYPE_HISTORY => false, + PosInterface::TX_TYPE_ORDER_HISTORY => true, + PosInterface::TX_TYPE_PAY_POST_AUTH => true, + PosInterface::TX_TYPE_CANCEL => true, + PosInterface::TX_TYPE_REFUND => true, + PosInterface::TX_TYPE_REFUND_PARTIAL => true, + PosInterface::TX_TYPE_STATUS => true, + PosInterface::TX_TYPE_CUSTOM_QUERY => true, + ]; + + /** + * @return ParamPosAccount + */ + public function getAccount(): AbstractPosAccount + { + return $this->account; + } + + // todo +// /** +// * @inheritDoc +// * +// * @throws UnsupportedTransactionTypeException +// * @throws \InvalidArgumentException when transaction type or payment model are not provided +// */ +// public function getApiURL(string $txType = null, string $paymentModel = null, ?string $orderTxType = null): string +// { +// if (null !== $txType && null !== $paymentModel) { +// return parent::getApiURL().'/'.$this->getRequestURIByTransactionType($txType, $paymentModel); +// } +// +// throw new \InvalidArgumentException('Transaction type and payment model are required to generate API URL'); +// } + + // todo + /** + * @inheritDoc + * + * @param string $threeDSessionId + */ + public function get3DGatewayURL(string $paymentModel = PosInterface::MODEL_3D_SECURE, string $threeDSessionId = null): string + { + if (PosInterface::MODEL_3D_HOST === $paymentModel) { + return parent::get3DGatewayURL($paymentModel).'/'.$threeDSessionId; + } + + return parent::get3DGatewayURL($paymentModel); + } + + // todo + /** + * @inheritDoc + */ + public function make3DPayment(Request $request, array $order, string $txType, CreditCardInterface $creditCard = null): PosInterface + { + throw new UnsupportedPaymentModelException(); + } + + // todo + /** + * @inheritDoc + */ + public function make3DPayPayment(Request $request, array $order, string $txType): PosInterface + { + $request = $request->request; + + if ($this->is3DAuthSuccess($request->all()) && !$this->requestDataMapper->getCrypt()->check3DHash($this->account, $request->all())) { + throw new HashMismatchException(); + } + + $this->response = $this->responseDataMapper->map3DPayResponseData($request->all(), $txType, $order); + + $this->logger->debug('finished 3D payment', ['mapped_response' => $this->response]); + + return $this; + } + + // todo + /** + * @inheritDoc + */ + public function make3DHostPayment(Request $request, array $order, string $txType): PosInterface + { + $request = $request->request; + + if ($this->is3DAuthSuccess($request->all()) && !$this->requestDataMapper->getCrypt()->check3DHash($this->account, $request->all())) { + throw new HashMismatchException(); + } + + $this->response = $this->responseDataMapper->map3DHostResponseData($request->all(), $txType, $order); + + $this->logger->debug('finished 3D payment', ['mapped_response' => $this->response]); + + return $this; + } + + // todo + /** + * @inheritDoc + */ + public function get3DFormData(array $order, string $paymentModel, string $txType, CreditCardInterface $creditCard = null, bool $createWithoutCard = true): array + { + $this->check3DFormInputs($paymentModel, $txType, $creditCard); + + $data = $this->registerPayment($order, $paymentModel, $txType, $creditCard); + + $status = $data['Code']; + + if (0 !== $status) { + $this->logger->error('payment register failed', $data); + + throw new \RuntimeException($data['Message'], $data['Code']); + } + + $this->logger->debug('preparing 3D form data'); + + return $this->requestDataMapper->create3DFormData( + $this->account, + $data, + $paymentModel, + $txType, + $this->get3DGatewayURL($paymentModel, $data['ThreeDSessionId'] ?? null), + $creditCard + ); + } + + // todo + /** + * @inheritDoc + */ + public function customQuery(array $requestData, string $apiUrl = null): PosInterface + { + if (null === $apiUrl) { + throw new \InvalidArgumentException('API URL is required for custom query'); + } + + return parent::customQuery($requestData, $apiUrl); + } + + // todo + /** + * @inheritDoc + */ + public function history(array $data): PosInterface + { + throw new UnsupportedTransactionTypeException(); + } + + // todo + /** + * @inheritDoc + * + * @return array + */ + protected function send($contents, string $txType, string $paymentModel, string $url): array + { + $this->logger->debug('sending request', ['url' => $url]); + + return $this->data = $this->sendSoapRequest($contents, $txType, $url); +// $response = $this->client->post($url, [ +// 'headers' => [ +// 'Content-Type' => 'application/json', +// ], +// 'body' => $contents, +// ]); + + $this->logger->debug('request completed', ['status_code' => $response->getStatusCode()]); + + if ($response->getStatusCode() === 204) { + $this->logger->warning('response from api is empty'); + + return $this->data = []; + } + + $responseContent = $response->getBody()->getContents(); + + return $this->data = $this->serializer->decode($responseContent, $txType); + } + + /** + * @phpstan-param PosInterface::TX_* + * + * @param array $contents + * @param string $txType + * @param string $url + * + * @return array + * + * @throws SoapFault + * @throws \RuntimeException + */ + private function sendSoapRequest(array $contents, string $txType, string $url): array + { + $this->logger->debug('sending soap request', [ + 'txType' => $txType, + 'url' => $url, + ]); + + $sslConfig = [ + 'allow_self_signed' => true, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]; + if ($this->isTestMode()) { + $sslConfig = [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]; + } + + $options = [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'stream_context' => stream_context_create(['ssl' => $sslConfig]), + 'exceptions' => true, + ]; + + + $client = new \SoapClient($url, $options); + try { + $result = $client->__soapCall( + 'TP_WMD_UCD', + [$contents] + //['parameters' => ['request' => $contents]] + ); + } catch (SoapFault $soapFault) { + $this->logger->error('soap error response', [ + 'message' => $soapFault->getMessage(), + ]); + + throw $soapFault; + } + + if (null === $result) { + $this->logger->error('Bankaya istek başarısız!', [ + 'response' => $result, + ]); + throw new \RuntimeException('Bankaya istek başarısız!'); + } + + $encodedResult = \json_encode($result); + + if (false === $encodedResult) { + return []; + } + + return $this->serializer->decode($encodedResult, $txType); + } + + + // todo + /** + * Ödeme İşlem Başlatma + * + * Ödeme formu ve Ortak Ödeme Sayfası ile ödeme işlemi başlatmak için ThreeDSessionId değeri üretilmelidir. + * Bu servis 3D secure başlatılması için session açar ve sessionId bilgisini döner. + * Bu servisten dönen ThreeDSessionId değeri ödeme formunda veya ortak ödeme sayfa çağırma işleminde kullanılır. + * + * @phpstan-param PosInterface::TX_TYPE_PAY_AUTH|PosInterface::TX_TYPE_PAY_PRE_AUTH $txType + * @phpstan-param PosInterface::MODEL_3D_* $paymentModel + * + * @param array $order + * @param string $paymentModel + * @param string $txType + * + * @return array + * + * @throws UnsupportedTransactionTypeException + * @throws ClientExceptionInterface + */ + private function registerPayment(array $order, string $paymentModel, string $txType, CreditCardInterface $creditCard): array + { + $requestData = $this->requestDataMapper->create3DEnrollmentCheckRequestData( + $this->account, + $order, + $creditCard, + ); + + $event = new RequestDataPreparedEvent( + $requestData, + $this->account->getBank(), + $txType, + \get_class($this), + $order, + $paymentModel + ); + /** @var RequestDataPreparedEvent $event */ + $event = $this->eventDispatcher->dispatch($event); + if ($requestData !== $event->getRequestData()) { + $this->logger->debug('Request data is changed via listeners', [ + 'txType' => $event->getTxType(), + 'bank' => $event->getBank(), + 'initialData' => $requestData, + 'updatedData' => $event->getRequestData(), + ]); + $requestData = $event->getRequestData(); + } + + $requestData = $this->serializer->encode($requestData, $txType); + + return $this->send( + $requestData, + $txType, + $paymentModel, + $this->getApiURL($txType, $paymentModel) + ); + } + + // todo + /** + * @phpstan-param PosInterface::TX_TYPE_* $txType + * @phpstan-param PosInterface::MODEL_* $paymentModel + * + * @return string + * + * @throws UnsupportedTransactionTypeException + */ + private function getRequestURIByTransactionType(string $txType, string $paymentModel): string + { + $arr = [ + PosInterface::TX_TYPE_PAY_AUTH => [ + PosInterface::MODEL_NON_SECURE => 'Payment', + PosInterface::MODEL_3D_PAY => 'threeDPayment', + PosInterface::MODEL_3D_HOST => 'threeDPayment', + ], + PosInterface::TX_TYPE_PAY_PRE_AUTH => [ + PosInterface::MODEL_3D_PAY => 'threeDPreAuth', + PosInterface::MODEL_3D_HOST => 'threeDPreAuth', + ], + PosInterface::TX_TYPE_PAY_POST_AUTH => 'postAuth', + PosInterface::TX_TYPE_CANCEL => 'void', + PosInterface::TX_TYPE_REFUND => 'refund', + PosInterface::TX_TYPE_REFUND_PARTIAL => 'refund', + PosInterface::TX_TYPE_STATUS => 'inquiry', + PosInterface::TX_TYPE_ORDER_HISTORY => 'history', + ]; + + if (!isset($arr[$txType])) { + throw new UnsupportedTransactionTypeException(); + } + + if (\is_string($arr[$txType])) { + return $arr[$txType]; + } + + if (!isset($arr[$txType][$paymentModel])) { + throw new UnsupportedTransactionTypeException(); + } + + return $arr[$txType][$paymentModel]; + } +} diff --git a/src/Serializer/ParamPosSerializer.php b/src/Serializer/ParamPosSerializer.php new file mode 100644 index 00000000..bb7b4674 --- /dev/null +++ b/src/Serializer/ParamPosSerializer.php @@ -0,0 +1,46 @@ +serializer = new Serializer([], [new JsonEncoder()]); + } + + /** + * @inheritDoc + */ + public static function supports(string $gatewayClass): bool + { + return $gatewayClass === ParamPos::class; + } + + /** + * @inheritDoc + */ + public function encode(array $data, ?string $txType = null): array + { + return $data; + } + + /** + * @inheritDoc + */ + public function decode(string $data, ?string $txType = null): array + { + dd($data); + return $this->serializer->decode($data, JsonEncoder::FORMAT); + } +}