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 @@
Akbank POS
+
+ Param POS
+
Payten V3
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);
+ }
+}