diff --git a/Controller/Test/PaymentGatewayFrontTestController.php b/Controller/Test/PaymentGatewayFrontTestController.php index 8d2dba0..a5f3dd5 100644 --- a/Controller/Test/PaymentGatewayFrontTestController.php +++ b/Controller/Test/PaymentGatewayFrontTestController.php @@ -82,6 +82,7 @@ public function configureAction(Request $request, string $configuration_alias) 'customer_id' => $transactionData['customer_id'], 'customer_email' => $transactionData['customer_email'], 'description' => $transactionData['description'], + 'metadata' => $transactionData['metadata'], ]); } diff --git a/Event/Subscriber/TransactionManagerEventSubscriber.php b/Event/Subscriber/TransactionManagerEventSubscriber.php index 5a3f864..ad9865e 100644 --- a/Event/Subscriber/TransactionManagerEventSubscriber.php +++ b/Event/Subscriber/TransactionManagerEventSubscriber.php @@ -39,6 +39,9 @@ public static function getSubscribedEvents() TransactionEvent::UNVERIFIED => [ ['save', 0], ], + TransactionEvent::UPDATED => [ + ['save', 0], + ], ]; } diff --git a/Event/TransactionEvent.php b/Event/TransactionEvent.php index 7585403..28854ee 100644 --- a/Event/TransactionEvent.php +++ b/Event/TransactionEvent.php @@ -13,6 +13,7 @@ class TransactionEvent extends Event const FAILED = 'idci_payment.transaction.failed'; const PENDING = 'idci_payment.transaction.pending'; const UNVERIFIED = 'idci_payment.transaction.unverified'; + const UPDATED = 'idci_payment.transaction.updated'; protected $transaction; diff --git a/Form/TransactionFormType.php b/Form/TransactionFormType.php index 7978de2..f3b9b28 100644 --- a/Form/TransactionFormType.php +++ b/Form/TransactionFormType.php @@ -6,6 +6,7 @@ use IDCI\Bundle\PaymentBundle\Manager\PaymentManager; use Payum\ISO4217\ISO4217; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type as Type; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -62,9 +63,25 @@ public function buildForm(FormBuilderInterface $builder, array $options) ->add('description', Type\TextareaType::class, [ 'required' => false, ]) + ->add('metadata', Type\TextareaType::class, [ + 'required' => false, + ]) ->add('submit', Type\SubmitType::class) ; + $builder->get('metadata')->addModelTransformer(new CallbackTransformer( + function ($metadata) { + if (null === $metadata) { + $metadata = []; + } + + return json_encode($metadata); + }, + function ($metadata) { + return json_decode($metadata, true); + } + )); + if (null !== $options['payment_gateway_configuration_alias']) { $builder->add('payment_gateway_configuration_alias', Type\HiddenType::class, [ 'data' => $options['payment_gateway_configuration_alias'], diff --git a/Gateway/AbstractPaymentGateway.php b/Gateway/AbstractPaymentGateway.php index ce4b9e9..8626be0 100644 --- a/Gateway/AbstractPaymentGateway.php +++ b/Gateway/AbstractPaymentGateway.php @@ -6,6 +6,7 @@ use IDCI\Bundle\PaymentBundle\Model\PaymentGatewayConfigurationInterface; use IDCI\Bundle\PaymentBundle\Model\Transaction; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; abstract class AbstractPaymentGateway implements PaymentGatewayInterface { @@ -14,9 +15,17 @@ abstract class AbstractPaymentGateway implements PaymentGatewayInterface */ protected $templating; - public function __construct(\Twig_Environment $templating) - { + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + + public function __construct( + \Twig_Environment $templating, + EventDispatcherInterface $dispatcher + ) { $this->templating = $templating; + $this->dispatcher = $dispatcher; } abstract public function initialize( diff --git a/Gateway/AtosSipsBinPaymentGateway.php b/Gateway/AtosSipsBinPaymentGateway.php index 2b64a8d..16ff257 100644 --- a/Gateway/AtosSipsBinPaymentGateway.php +++ b/Gateway/AtosSipsBinPaymentGateway.php @@ -10,6 +10,7 @@ use Payum\ISO4217\ISO4217; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Process\Process; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class AtosSipsBinPaymentGateway extends AbstractPaymentGateway { @@ -30,11 +31,12 @@ class AtosSipsBinPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $pathfile, string $requestBinPath, string $responseBinPath ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->pathfile = $pathfile; $this->requestBinPath = $requestBinPath; diff --git a/Gateway/AtosSipsJsonPaymentGateway.php b/Gateway/AtosSipsJsonPaymentGateway.php index ac6b10a..16713de 100644 --- a/Gateway/AtosSipsJsonPaymentGateway.php +++ b/Gateway/AtosSipsJsonPaymentGateway.php @@ -11,6 +11,7 @@ use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; use Payum\ISO4217\ISO4217; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class AtosSipsJsonPaymentGateway extends AbstractPaymentGateway { @@ -21,9 +22,10 @@ class AtosSipsJsonPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $serverHostName ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->serverHostName = $serverHostName; } diff --git a/Gateway/AtosSipsPostPaymentGateway.php b/Gateway/AtosSipsPostPaymentGateway.php index ad4b6f7..936cb85 100644 --- a/Gateway/AtosSipsPostPaymentGateway.php +++ b/Gateway/AtosSipsPostPaymentGateway.php @@ -9,6 +9,7 @@ use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; use Payum\ISO4217\ISO4217; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class AtosSipsPostPaymentGateway extends AbstractPaymentGateway { @@ -19,9 +20,10 @@ class AtosSipsPostPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $serverHostName ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->serverHostName = $serverHostName; } diff --git a/Gateway/Client/EurekaPaymentGatewayClient.php b/Gateway/Client/EurekaPaymentGatewayClient.php new file mode 100644 index 0000000..1933e4d --- /dev/null +++ b/Gateway/Client/EurekaPaymentGatewayClient.php @@ -0,0 +1,1294 @@ +templating = $templating; + $this->client = new Client(['defaults' => ['verify' => false, 'timeout' => 5]]); + $this->cache = null; + $this->serverHostName = $serverHostName; + } + + public function setCache($cache) + { + if (null !== $cache) { + if (!interface_exists(AdapterInterface::class)) { + throw new \RuntimeException('ConfigurationFetcher cache requires "symfony/cache" package'); + } + + if (!$cache instanceof AdapterInterface) { + throw new \UnexpectedValueException( + sprintf('The fetcher\'s cache adapter must implement %s.', AdapterInterface::class) + ); + } + + $this->cache = $cache; + } + } + + public function getSTSConnectionUrl(): string + { + return sprintf('https://paymentsts.%s/Users/soapIssue.svc', $this->serverHostName); + } + + public function getMerchantUrl(): string + { + return sprintf('https://paymentservices.%s/MerchantGatewayFrontService.svc', $this->serverHostName); + } + + public function getScoreV3Url(): string + { + return sprintf('https://services.%s/Cb4xFrontService.svc', $this->serverHostName); + } + + public function getScoreCclUrl(): string + { + return sprintf('https://services.%s/CclFrontService.svc', $this->serverHostName); + } + + public function getPaymentFormUrl(): string + { + return sprintf('https://payment.%s/V4/GenericRD/Redirect.aspx', $this->serverHostName); + } + + private function getSTSTokenHash(string $username) + { + return sprintf('idci_payment.eureka.sts_token.%s', md5($username)); + } + + public function getSTSTokenResponse(string $username, string $password) + { + return $this->client->request('POST', $this->getSTSConnectionUrl(), [ + 'body' => $this->templating->render('@IDCIPayment/Gateway/eureka/sts_token.xml.twig', [ + 'username' => $username, + 'password' => $password, + 'merchant_url' => $this->getMerchantUrl(), + ]), + 'headers' => [ + 'Content-Type' => 'text/xml', + 'SOAPAction' => 'http://www.cdiscount.com/SoapTokenServiceContract/Issue', + ], + ]); + } + + public function getSTSToken(string $username, string $password) + { + if (null !== $this->cache && $this->cache->hasItem($this->getSTSTokenHash($username))) { + return $this->cache->getItem($this->getSTSTokenHash($username))->get(); + } + + $token = (new Crawler((string) $this->getSTSTokenResponse($username, $password)->getBody()))->filterXPath('//issueresult')->text(); + + if (null !== $this->cache) { + $item = $this->cache->getItem($this->getSTSTokenHash($username)); + $item->set($token); + $item->expiresAfter(self::TOKEN_STS_CACHE_TTL); + + $this->cache->save($item); + } + + return $token; + } + + public function getScoringTokenResponse(string $type, array $options) + { + if (self::SCORE_V3 !== $type && self::SCORE_CCL !== $type) { + throw new \InvalidArgumentException( + sprintf('The scoring type "%s" is not supported. Supported values: %s, %s', $type, self::SCORE_V3, self::SCORE_CCL) + ); + } + + return $this->client->request( + 'POST', + self::SCORE_V3 === $type ? $this->getScoreV3Url() : $this->getScoreCclUrl(), + [ + 'body' => $this->templating->render( + '@IDCIPayment/Gateway/eureka/score.xml.twig', + $this->resolveScoreOptions($options) + ), + 'headers' => [ + 'Content-Type' => 'text/xml', + 'SOAPAction' => 'http://www.cb4x.fr/ICb4xFrontService/Score', + ], + ] + ); + } + + public function getScoringToken(string $type, array $options) + { + $crawler = (new Crawler((string) $this->getScoringTokenResponse($type, $options)->getBody())); + + if ('false' === $crawler->filterXPath('//paymentagreement')->text()) { + throw new \UnexpectedValueException( + sprintf('The scoring token request failed: %s', $crawler->filterXPath('//responsemessage')->text()) + ); + } + + $scoringToken = $crawler->filterXPath('//scoringtoken')->text(); + + return $scoringToken; + } + + public function payOrderRank(array $options) + { + return $this->client->request('POST', $this->getMerchantUrl(), [ + 'body' => $this->templating->render('@IDCIPayment/Gateway/eureka/pay_order_rank.xml.twig', $this->resolvePayOrderRankOptions($options)), + 'headers' => [ + 'Content-Type' => 'text/xml', + 'SoapAction' => 'PayOrderRank', + ], + ]); + } + + public function updateOrder(array $options) + { + return $this->client->request('POST', $this->getMerchantUrl(), [ + 'body' => $this->templating->render('@IDCIPayment/Gateway/eureka/update_order.xml.twig', $this->resolveUpdateOrderOptions($options)), + 'headers' => [ + 'Content-Type' => 'text/xml', + 'SoapAction' => 'UpdateOrder', + ], + ]); + } + + private function resolveScoreOptions(array $scoreOptions): array + { + $scoreResolver = (new OptionsResolver()) + ->setRequired([ + 'Header', + 'Request', + ]) + ->setAllowedTypes('Header', ['array']) + ->setNormalizer('Header', function (Options $options, $value) { + return $this->resolveHeaderOptions($value); + }) + ->setAllowedTypes('Request', ['array']) + ->setNormalizer('Request', function (Options $options, $value) { + return $this->resolveRequestOptions($value); + }) + ; + + return $scoreResolver->resolve($scoreOptions); + } + + private function resolvePayOrderRankOptions(array $payOrderRankOptions): array + { + $payOrderRankResolver = (new OptionsResolver()) + ->setRequired([ + 'Header', + 'PayOrderRankRequestMessage', + ]) + ->setAllowedTypes('Header', ['array']) + ->setNormalizer('Header', function (Options $options, $value) { + return $this->resolveHeaderOptions($value); + }) + ->setAllowedTypes('PayOrderRankRequestMessage', ['array']) + ->setNormalizer('PayOrderRankRequestMessage', function (Options $options, $value) { + return $this->resolvePayOrderRankRequestMessageOptions($value); + }) + ; + + return $payOrderRankResolver->resolve($payOrderRankOptions); + } + + private function resolveUpdateOrderOptions(array $payOrderRankOptions): array + { + $payOrderRankResolver = (new OptionsResolver()) + ->setRequired([ + 'Header', + 'UpdateOrderRequestMessage', + ]) + ->setAllowedTypes('Header', ['array']) + ->setNormalizer('Header', function (Options $options, $value) { + return $this->resolveHeaderOptions($value); + }) + ->setAllowedTypes('UpdateOrderRequestMessage', ['array']) + ->setNormalizer('UpdateOrderRequestMessage', function (Options $options, $value) { + return $this->resolveUpdateOrderRequestMessageOptions($value); + }) + ; + + return $payOrderRankResolver->resolve($payOrderRankOptions); + } + + private function resolveHeaderOptions(array $headerOptions): array + { + $headerResolver = (new OptionsResolver()) + ->setRequired([ + 'Context', + 'Localization', + 'SecurityContext', + ]) + ->setDefaults([ + 'Version' => 1, + ]) + ->setAllowedTypes('Context', ['array']) + ->setNormalizer('Context', function (Options $options, $value) { + return $this->resolveContextOptions($value); + }) + ->setAllowedTypes('Localization', ['array']) + ->setNormalizer('Localization', function (Options $options, $value) { + return $this->resolveLocalizationOptions($value); + }) + ->setAllowedTypes('SecurityContext', ['array']) + ->setNormalizer('SecurityContext', function (Options $options, $value) { + return $this->resolveSecurityContextOptions($value); + }) + ->setAllowedTypes('Version', ['int', 'string', 'float']) + ; + + return $headerResolver->resolve($headerOptions); + } + + private function resolveContextOptions($contextOptions): array + { + $contextResolver = (new OptionsResolver()) + ->setRequired([ + 'MerchantId', + 'MerchantSiteId', + ]) + ->setAllowedTypes('MerchantId', ['int', 'string']) + ->setAllowedTypes('MerchantSiteId', ['int', 'string']) + ; + + return $contextResolver->resolve($contextOptions); + } + + private function resolveLocalizationOptions($localizationOptions): array + { + $localizationResolver = (new OptionsResolver()) + ->setRequired([ + 'Country', + 'Currency', + 'Language', + ]) + ->setDefaults([ + 'DecimalPosition' => 2, + ]) + ->setAllowedTypes('Country', ['string']) + ->setAllowedTypes('Currency', ['string']) + ->setAllowedValues('Currency', array_map(function ($currency) { + return $currency->getAlpha3(); + }, (new ISO4217())->findAll())) + ->setAllowedTypes('DecimalPosition', ['int']) + ->setAllowedTypes('Language', ['string']) + ; + + return $localizationResolver->resolve($localizationOptions); + } + + private function resolveSecurityContextOptions($securityContextOptions): array + { + $securityContextResolver = (new OptionsResolver()) + ->setRequired([ + 'TokenId', + ]) + ->setAllowedTypes('TokenId', ['string']) + ; + + return $securityContextResolver->resolve($securityContextOptions); + } + + private function resolvePayOrderRankRequestMessageOptions(array $payOrderRankRequestMessageOptions): array + { + $payOrderRankRequestMessageResolver = (new OptionsResolver()) + ->setRequired([ + 'Amount', + 'OrderRef', + ]) + ->setDefaults([ + 'Attempt' => 1, + 'Rank' => 1, + ]) + ->setAllowedTypes('Amount', ['int']) + ->setAllowedTypes('OrderRef', ['string']) + ->setAllowedTypes('Attempt', ['int']) + ->setAllowedTypes('Rank', ['int']) + ; + + return $payOrderRankRequestMessageResolver->resolve($payOrderRankRequestMessageOptions); + } + + private function resolveUpdateOrderRequestMessageOptions(array $updateOrderRequestMessageOptions): array + { + $updateOrderRequestMessageResolver = (new OptionsResolver()) + ->setRequired([ + 'NewAmount', + 'OrderRef', + 'ScoringToken', + ]) + ->setAllowedTypes('NewAmount', ['int']) + ->setAllowedTypes('OrderRef', ['string']) + ->setAllowedTypes('ScoringToken', ['string']) + ; + + return $updateOrderRequestMessageResolver->resolve($updateOrderRequestMessageOptions); + } + + private function resolveRequestOptions(array $contextOptions): array + { + $contextResolver = (new OptionsResolver()) + ->setRequired([ + 'Customer', + 'Order', + ]) + ->setDefaults([ + 'OptionalCustomerHistory' => [], + 'OptionalTravelDetails' => [], + 'OptionalStayDetails' => [], + 'OptionalProductDetails' => [], + 'OptionalPreScoreInformation' => [], + 'AdditionalNumericFieldList' => [], + 'AdditionalTextFieldList' => [], + 'OptionalShippingDetails' => [], + ]) + ->setAllowedTypes('Customer', ['array']) + ->setNormalizer('Customer', function (Options $options, $value) { + return $this->resolveCustomerOptions($value); + }) + ->setAllowedTypes('Order', ['array']) + ->setNormalizer('Order', function (Options $options, $value) { + return $this->resolveOrderOptions($value); + }) + ->setAllowedTypes('OptionalCustomerHistory', ['array']) + ->setNormalizer('OptionalCustomerHistory', function (Options $options, $value) { + return $this->resolveOptionalCustomerHistoryOptions($value); + }) + ->setAllowedTypes('OptionalTravelDetails', ['array']) + ->setNormalizer('OptionalTravelDetails', function (Options $options, $value) { + return $this->resolveOptionalTravelDetailsOptions($value); + }) + ->setAllowedTypes('OptionalStayDetails', ['array']) + ->setNormalizer('OptionalStayDetails', function (Options $options, $value) { + return $this->resolveOptionalStayDetailsOptions($value); + }) + ->setAllowedTypes('OptionalProductDetails', ['array']) + ->setNormalizer('OptionalProductDetails', function (Options $options, $value) { + return $this->resolveOptionalProductDetailsOptions($value); + }) + ->setAllowedTypes('OptionalPreScoreInformation', ['array']) + ->setNormalizer('OptionalPreScoreInformation', function (Options $options, $value) { + return $this->resolveOptionalPreScoreInformationOptions($value); + }) + ->setAllowedTypes('AdditionalNumericFieldList', ['null', 'array']) + ->setNormalizer('AdditionalNumericFieldList', function (Options $options, $value) { + return $this->resolveAdditionalFieldListOptions($value); + }) + ->setAllowedTypes('AdditionalTextFieldList', ['null', 'array']) + ->setNormalizer('AdditionalTextFieldList', function (Options $options, $value) { + return $this->resolveAdditionalFieldListOptions($value); + }) + ->setAllowedTypes('OptionalShippingDetails', ['array']) + ->setNormalizer('OptionalShippingDetails', function (Options $options, $value) { + return $this->resolveOptionalShippingDetailsOptions($value); + }) + ; + + return $contextResolver->resolve($contextOptions); + } + + private function resolveCustomerOptions(array $customerOptions): array + { + $customerResolver = (new OptionsResolver()) + ->setRequired([ + 'CustomerRef', + 'LastName', + 'FirstName', + 'Civility', + 'BirthDate', + 'BirthZipCode', + 'PhoneNumber', + 'CellPhoneNumber', + 'Email', + 'Address1', + 'ZipCode', + 'City', + 'Country', + ]) + ->setDefaults([ + 'Address2' => null, + 'Address3' => null, + 'Address4' => null, + 'MaidenName' => null, + 'Nationality' => null, + 'IpAddress' => null, + 'WhiteList' => null, + ]) + ->setAllowedTypes('CustomerRef', ['int', 'string']) + ->setNormalizer('CustomerRef', function (Options $options, $value) { + if (strlen((string) $value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "Customer.CustomerRef" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('LastName', ['string']) + ->setNormalizer('LastName', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 64) { + throw new \InvalidArgumentException( + sprintf('The "Customer.LastName" max length is 64, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('FirstName', ['string']) + ->setNormalizer('FirstName', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 64) { + throw new \InvalidArgumentException( + sprintf('The "Customer.FirstName" max length is 64, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Civility', ['string']) + ->setAllowedValues('Civility', [ + self::CIVILITY_MISTER, + self::CIVILITY_MISS, + self::CIVILITY_MISSTRESS, + strtolower(self::CIVILITY_MISTER), + strtolower(self::CIVILITY_MISS), + strtolower(self::CIVILITY_MISSTRESS), + ]) + ->setNormalizer('Civility', function (Options $options, $value) { + return ucfirst($value); + }) + ->setAllowedTypes('MaidenName', ['null', 'string']) + ->setNormalizer('MaidenName', function (Options $options, $value) { + if (self::CIVILITY_MISSTRESS !== $options['Civility']) { + return null; + } + + if (self::CIVILITY_MISSTRESS === $options['Civility'] && null === $value) { + throw new \UnexpectedValueException( + sprintf( + 'As the field "Customer.Civility" of the customer equal to "%s", "Customer.MaidenName" mustn\'t be null.', + self::CIVILITY_MISSTRESS + ) + ); + } + + return $value; + }) + ->setAllowedTypes('BirthDate', ['string', \DateTime::class]) + ->setNormalizer('BirthDate', function (Options $options, $value) { + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "Customer.BirthDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + + ->setAllowedTypes('BirthZipCode', ['int', 'string']) + ->setNormalizer('BirthZipCode', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 5) { + throw new \InvalidArgumentException( + sprintf('The "Customer.BirthZipCode" max length is 5, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('PhoneNumber', ['string']) + ->setNormalizer('PhoneNumber', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 13) { + throw new \InvalidArgumentException( + sprintf('The "Customer.PhoneNumber" max length is 13, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('CellPhoneNumber', ['string']) + ->setNormalizer('CellPhoneNumber', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 13) { + throw new \InvalidArgumentException( + sprintf('The "Customer.CellPhoneNumber" max length is 13, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Email', ['string']) + ->setNormalizer('Email', function (Options $options, $value) { + if (strlen($value) > 60) { + throw new \InvalidArgumentException( + sprintf('The "Customer.Email" max length is 60, current size given: %s', strlen($value)) + ); + } + + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException( + sprintf('The parameter given in "Customer.Email" is not a valid email (%s).', $value) + ); + } + + return $value; + }) + ->setAllowedTypes('Address1', ['string']) + ->setNormalizer('Address1', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 32) { + throw new \InvalidArgumentException( + sprintf('The "Customer.Address1" max length is 32, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Address2', ['null', 'string']) + ->setNormalizer('Address2', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 32) { + throw new \InvalidArgumentException( + sprintf('The "Customer.Address2" max length is 32, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Address3', ['null', 'string']) + ->setNormalizer('Address3', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 32) { + throw new \InvalidArgumentException( + sprintf('The "Customer.Address3" max length is 32, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Address4', ['null', 'string']) + ->setNormalizer('Address4', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 32) { + throw new \InvalidArgumentException( + sprintf('The "Customer.Address4" max length is 32, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ZipCode', ['int', 'string']) + ->setNormalizer('ZipCode', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 5) { + throw new \InvalidArgumentException( + sprintf('The "Customer.ZipCode" max length is 5, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('City', ['string']) + ->setNormalizer('City', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 50) { + throw new \InvalidArgumentException( + sprintf('The "Customer.City" max length is 50, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Country', ['string']) + ->setAllowedTypes('Nationality', ['null', 'string']) + ->setAllowedValues('Nationality', [ + null, + self::NATIONALITY_FRANCE, + self::NATIONALITY_EUROPEAN_UNION, + self::NATIONALITY_OTHER, + ]) + ->setAllowedTypes('IpAddress', ['null', 'string']) + ->setNormalizer('IpAddress', function (Options $options, $value) { + if (is_string($value) && !filter_var($value, FILTER_VALIDATE_IP)) { + throw new \InvalidArgumentException( + sprintf('The parameter given in "Customer.IpAddress" is not a IPv4 (%s).', $value) + ); + } + + return $value; + }) + ->setAllowedTypes('WhiteList', ['null', 'string']) + ->setAllowedValues('WhiteList', [ + null, + self::WHITELIST_STATUS_BLACKLIST, + self::WHITELIST_STATUS_UNKNOWN, + self::WHITELIST_STATUS_TRUSTED, + self::WHITELIST_STATUS_WHITELIST, + ]) + ; + + return $customerResolver->resolve($customerOptions); + } + + private function resolveOrderOptions(array $orderOptions): array + { + $orderResolver = (new OptionsResolver()) + ->setRequired([ + 'OrderDate', + 'SaleChannel', + 'ShippingMethod', + 'ShoppingCartItemCount', + 'ShoppingCartRef', + 'TotalAmount', + ]) + ->setAllowedTypes('OrderDate', ['string']) + ->setAllowedTypes('SaleChannel', ['string']) + ->setAllowedValues('SaleChannel', [ + self::SALE_CHANNEL_DESKTOP, + self::SALE_CHANNEL_TABLET, + self::SALE_CHANNEL_TABLED_IPAD, + self::SALE_CHANNEL_SMARTPHONE, + self::SALE_CHANNEL_SMARTPHONE_ANDROID, + self::SALE_CHANNEL_SMARTPHONE_IPHONE, + ]) + ->setAllowedTypes('ShippingMethod', ['string']) + ->setAllowedValues('ShippingMethod', [ + self::SHIPPING_METHOD_COLISSIMO_DIRECT, + self::SHIPPING_METHOD_CHRONOPOST, + self::SHIPPING_METHOD_COLISSIMO, + self::SHIPPING_METHOD_CHRONORELAIS, + self::SHIPPING_METHOD_KIALA, + self::SHIPPING_METHOD_IMPRESSION, + self::SHIPPING_METHOD_LIVRAISON_SERVICE_PLUS, + self::SHIPPING_METHOD_MORY, + self::SHIPPING_METHOD_RELAIS_CDISCOUNT, + self::SHIPPING_METHOD_TNT, + self::SHIPPING_METHOD_TRANSPORTEUR, + self::SHIPPING_METHOD_EASYDIS_ERREUR, + self::SHIPPING_METHOD_EASYDIS, + self::SHIPPING_METHOD_KIB, + self::SHIPPING_METHOD_TNT_BELGIQUE, + self::SHIPPING_METHOD_LIVRAISON_EXPRESS, + self::SHIPPING_METHOD_AGEDISS, + self::SHIPPING_METHOD_EMPORTE, + self::SHIPPING_METHOD_EMPORTE_MOINS_30, + self::SHIPPING_METHOD_ADREXO, + self::SHIPPING_METHOD_EMPORTE_MOINS_30_EASYDIS, + self::SHIPPING_METHOD_VIRTUEL, + self::SHIPPING_METHOD_RECOMMANDE, + self::SHIPPING_METHOD_NORMAL, + self::SHIPPING_METHOD_SUIVI, + self::SHIPPING_METHOD_PREMIUM_EASYDIS, + self::SHIPPING_METHOD_CONFORT_EASYDIS, + self::SHIPPING_METHOD_RELAIS_CESTAS, + self::SHIPPING_METHOD_SO_COLLISIMO_ZONE_1, + self::SHIPPING_METHOD_SO_COLLISIMO_ZONE_2, + self::SHIPPING_METHOD_RETRAIT_IMMEDIAT_MAGASIN, + self::SHIPPING_METHOD_LDR, + self::SHIPPING_METHOD_LIVRAISON_EN_MAGASIN, + self::SHIPPING_METHOD_ECO_EASYDIS, + self::SHIPPING_METHOD_MODIAL_RELAY, + self::SHIPPING_METHOD_FOURNISSEUR_DIRECT_RELAIS, + self::SHIPPING_METHOD_TNT_EXPRESS_RELAIS, + self::SHIPPING_METHOD_EXPRESS, + self::SHIPPING_METHOD_EMPORTE_CHRONOPOST_RELAI, + self::SHIPPING_METHOD_EMPORTE_CHRONOPOST_CONSIGNE, + ]) + ->setAllowedTypes('ShoppingCartItemCount', ['int']) + ->setAllowedTypes('ShoppingCartRef', ['int', 'string']) + ->setAllowedTypes('TotalAmount', ['int']) + ; + + return $orderResolver->resolve($orderOptions); + } + + private function resolveOptionalCustomerHistoryOptions(array $optionalCustomerHistory): array + { + $optionalCustomerHistoryResolver = (new OptionsResolver()) + ->setRequired([ + 'CanceledOrderAmount', + 'CanceledOrderCount', + 'FirstOrderDate', + 'FraudAlertCount', + 'LastOrderDate', + 'PaymentIncidentCount', + 'RefusedManyTimesOrderCount', + 'UnvalidatedOrderCount', + 'ValidatedOneTimeOrderCount', + 'ValidatedOrderCount', + 'ClientIpAddressRecurrence', + 'OngoingLitigationOrderAmount', + 'PaidLitigationOrderAmount24Month', + 'ScoreSimulationCount7Days', + ]) + ->setAllowedTypes('CanceledOrderAmount', ['null', 'int']) + ->setAllowedTypes('CanceledOrderCount', ['null', 'int']) + ->setAllowedTypes('FirstOrderDate', ['null', 'string', \DateTime::class]) + ->setNormalizer('FirstOrderDate', function (Options $options, $value) { + if (null === $value) { + return $value; + } + + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (is_string($value) && 1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "OptionalCustomerHistory.FirstOrderDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + ->setAllowedTypes('FraudAlertCount', ['null', 'int']) + ->setAllowedTypes('LastOrderDate', ['null', 'string', \DateTime::class]) + ->setNormalizer('LastOrderDate', function (Options $options, $value) { + if (null === $value) { + return $value; + } + + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (is_string($value) && 1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "OptionalCustomerHistory.LastOrderDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + ->setAllowedTypes('PaymentIncidentCount', ['null', 'int']) + ->setAllowedTypes('RefusedManyTimesOrderCount', ['null', 'int']) + ->setAllowedTypes('UnvalidatedOrderCount', ['null', 'int']) + ->setAllowedTypes('ValidatedOneTimeOrderCount', ['null', 'int']) + ->setAllowedTypes('ValidatedOrderCount', ['null', 'int']) + ->setAllowedTypes('ClientIpAddressRecurrence', ['null', 'int']) + ->setAllowedTypes('OngoingLitigationOrderAmount', ['null', 'int']) + ->setAllowedTypes('PaidLitigationOrderAmount24Month', ['null', 'int']) + ->setAllowedTypes('ScoreSimulationCount7Days', ['null', 'int']) + ; + + $optionalCustomerHistory = $optionalCustomerHistoryResolver->resolve($optionalCustomerHistory); + if (empty(array_filter($optionalCustomerHistory, function ($a) { return null !== $a; }))) { + return []; + } + + return $optionalCustomerHistory; + } + + private function resolveOptionalTravelDetailsOptions(array $optionalTravelDetails): array + { + $optionalTravelDetailsResolver = (new OptionsResolver()) + ->setRequired([ + 'Insurance', + 'Type', + 'DepartureDate', + 'ReturnDate', + 'DestinationCountry', + 'TicketCount', + 'TravellerCount', + 'Class', + 'OwnTicket', + 'MainDepartureCompany', + 'DepartureAirport', + 'ArrivalAirport', + 'DiscountCode', + 'LuggageSupplement', + 'ModificationAnnulation', + 'TravellerPassportList', + ]) + ->setAllowedTypes('Insurance', ['null', 'string']) + ->setNormalizer('Insurance', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.Insurance" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Type', ['null', 'string']) + ->setAllowedValues('Type', [ + null, + self::UNKNOWN_TRAVEL_TYPE, + self::ONE_WAY_TRAVEL_TYPE, + self::TWO_WAY_TRAVEL_TYPE, + self::MULTIPLE_TRAVEL_TYPE, + ]) + ->setAllowedTypes('DepartureDate', ['null', 'string', \DateTime::class]) + ->setNormalizer('DepartureDate', function (Options $options, $value) { + if (null === $value) { + return $value; + } + + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (is_string($value) && 1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "OptionalTravelDetails.DepartureDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + ->setAllowedTypes('ReturnDate', ['null', 'string', \DateTime::class]) + ->setNormalizer('ReturnDate', function (Options $options, $value) { + if (null === $value) { + return $value; + } + + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (is_string($value) && 1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "OptionalTravelDetails.ReturnDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + ->setAllowedTypes('DestinationCountry', ['null', 'string']) + ->setNormalizer('DestinationCountry', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 2) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.DestinationCountry" max length is 2, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('TicketCount', ['null', 'int']) + ->setAllowedTypes('TravellerCount', ['null', 'int']) + ->setAllowedTypes('Class', ['null', 'string']) + ->setAllowedValues('Class', [ + null, + self::UNKNOWN_TRAVEL_CLASS, + self::ECONOMY_TRAVEL_CLASS, + self::PREMIUM_ECONOMY_TRAVEL_CLASS, + self::BUSINESS_TRAVEL_CLASS, + self::FIRST_TRAVEL_CLASS, + self::OTHER_TRAVEL_CLASS, + ]) + ->setAllowedTypes('OwnTicket', ['null', 'bool']) + ->setAllowedTypes('MainDepartureCompany', ['null', 'string']) + ->setNormalizer('MainDepartureCompany', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 3) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.MainDepartureCompany" max length is 3, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('DepartureAirport', ['null', 'string']) + ->setNormalizer('DepartureAirport', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 3) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.DepartureAirport" max length is 3, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ArrivalAirport', ['null', 'string']) + ->setNormalizer('ArrivalAirport', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 3) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.ArrivalAirport" max length is 3, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('DiscountCode', ['null', 'string']) + ->setNormalizer('DiscountCode', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.DiscountCode" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('LuggageSupplement', ['null', 'string']) + ->setNormalizer('LuggageSupplement', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.LuggageSupplement" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ModificationAnnulation', ['null', 'bool']) + ->setAllowedTypes('TravellerPassportList', ['null', 'array']) + ->setNormalizer('TravellerPassportList', function (Options $options, $value) { + return $this->resolveTravellerPassportListOptions($value); + }) + ; + + $optionalTravelDetails = $optionalTravelDetailsResolver->resolve($optionalTravelDetails); + if (empty(array_filter($optionalTravelDetails, function ($a) { return null !== $a && (is_array($a) && !empty($a)); }))) { + return []; + } + + return $optionalTravelDetails; + } + + private function resolveTravellerPassportListOptions(?array $travellerPassportList): array + { + if (null === $travellerPassportList || empty($travellerPassportList)) { + return []; + } + + $travellerPassportListResolver = (new OptionsResolver()) + ->setRequired([ + 'ExpirationDate', + 'IssuanceCountry', + ]) + > setAllowedTypes('ExpirationDate', ['null', 'string', \DateTime::class]) + ->setNormalizer('ExpirationDate', function (Options $options, $value) { + if (null === $value) { + return $value; + } + + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d'); + } + + if (is_string($value) && 1 !== preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}/', $value)) { + throw new \InvalidArgumentException( + 'The "OptionalTravelDetails.TravellerPassportList[].ExpirationDate" must be formatted as described in documentation "YYYY-MM-DD"' + ); + } + + return $value; + }) + ->setAllowedTypes('IssuanceCountry', ['null', 'string']) + ->setNormalizer('IssuanceCountry', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 2) { + throw new \InvalidArgumentException( + sprintf('The "OptionalTravelDetails.TravellerPassportList[]IssuanceCountry" max length is 2, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ; + + $resolvedTravellerPassportList = []; + foreach ($value as $travellerPassport) { + $resolvedTravellerPassportList[] = $travellerPassportListResolver->resolve($travellerPassport); + } + + return !empty($resolvedTravellerPassportList) ? $resolvedTravellerPassportList : []; + } + + private function resolveOptionalStayDetailsOptions(array $optionalStayDetails): array + { + $optionalStayDetailsResolver = (new OptionsResolver()) + ->setRequired([ + 'Company', + 'Destination', + 'NightNumber', + 'RoomRange', + ]) + ->setAllowedTypes('Company', ['null', 'string']) + ->setNormalizer('Company', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 50) { + throw new \InvalidArgumentException( + sprintf('The "OptionalStayDetails.Company" max length is 50, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Destination', ['null', 'string']) + ->setNormalizer('Destination', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 50) { + throw new \InvalidArgumentException( + sprintf('The "OptionalStayDetails.Destination" max length is 50, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('NightNumber', ['null', 'int']) + ->setAllowedTypes('RoomRange', ['null', 'int']) + ; + + $optionalStayDetails = $optionalStayDetailsResolver->resolve($optionalStayDetails); + if (empty(array_filter($optionalStayDetails, function ($a) { return null !== $a; }))) { + return []; + } + + return $optionalStayDetails; + } + + private function resolveOptionalProductDetailsOptions(array $optionalProductDetails): array + { + $optionalProductDetailsResolver = (new OptionsResolver()) + ->setRequired([ + 'Categorie1', + 'Categorie2', + 'Categorie3', + ]) + ->setAllowedTypes('Categorie1', ['null', 'string']) + ->setNormalizer('Categorie1', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalProductDetails.Categorie1" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Categorie2', ['null', 'string']) + ->setNormalizer('Categorie2', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalProductDetails.Categorie2" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('Categorie3', ['null', 'string']) + ->setNormalizer('Categorie3', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 30) { + throw new \InvalidArgumentException( + sprintf('The "OptionalProductDetails.Categorie3" max length is 30, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ; + + $optionalProductDetails = $optionalProductDetailsResolver->resolve($optionalProductDetails); + if (empty(array_filter($optionalProductDetails, function ($a) { return null !== $a; }))) { + return []; + } + + return $optionalProductDetails; + } + + private function resolveOptionalPreScoreInformationOptions(array $optionalPreScoreInformation): array + { + $optionalPreScoreInformationResolver = (new OptionsResolver()) + ->setRequired([ + 'RequestID', + ]) + ->setAllowedTypes('RequestID', ['null', 'string']) + ; + + $optionalPreScoreInformation = $optionalPreScoreInformationResolver->resolve($optionalPreScoreInformation); + if (empty(array_filter($optionalPreScoreInformation, function ($a) { return null !== $a; }))) { + return []; + } + + return $optionalPreScoreInformation; + } + + private function resolveAdditionalFieldListOptions(?array $additionalFieldList): array + { + if (null === $additionalFieldList || empty($additionalFieldList)) { + return []; + } + + $additionalNumericFieldListResolver = (new OptionsResolver()) + ->setRequired([ + 'Index', + 'Value', + ]) + ; + + $additionalNumericFieldList = []; + foreach ($additionalFieldList as $additionalNumericField) { + $additionalNumericFieldList[] = $additionalNumericFieldListResolver->resolve($additionalNumericField); + } + + return !empty($additionalNumericFieldList) ? $additionalNumericFieldList : []; + } + + private function resolveOptionalShippingDetailsOptions(array $orderShippingDetails): array + { + $orderShippingDetailsResolver = (new OptionsResolver()) + ->setRequired([ + 'ShippingAdress1', + 'ShippingAdress2', + 'ShippingAdressCity', + 'ShippingAdressZip', + 'ShippingAdressCountry', + ]) + ->setAllowedTypes('ShippingAdress1', ['null', 'string']) + ->setNormalizer('ShippingAdress1', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 100) { + throw new \InvalidArgumentException( + sprintf('The "OrderShippingDetails.ShippingAdress1" max length is 100, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ShippingAdress2', ['null', 'string']) + ->setNormalizer('ShippingAdress2', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 100) { + throw new \InvalidArgumentException( + sprintf('The "OrderShippingDetails.ShippingAdress2" max length is 100, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ShippingAdressCity', ['null', 'string']) + ->setNormalizer('ShippingAdressCity', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 100) { + throw new \InvalidArgumentException( + sprintf('The "OrderShippingDetails.ShippingAdressCity" max length is 100, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ShippingAdressZip', ['null', 'string']) + ->setNormalizer('ShippingAdressZip', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 5) { + throw new \InvalidArgumentException( + sprintf('The "OrderShippingDetails.ShippingAdressZip" max length is 5, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ->setAllowedTypes('ShippingAdressCountry', ['null', 'string']) + ->setNormalizer('ShippingAdressCountry', function (Options $options, $value) { + if (is_string($value) && strlen($value) > 2) { + throw new \InvalidArgumentException( + sprintf('The "OrderShippingDetails.ShippingAdressCountry" max length is 2, current size given: %s', strlen($value)) + ); + } + + return $value; + }) + ; + + $orderShippingDetails = $orderShippingDetailsResolver->resolve($orderShippingDetails); + if (empty(array_filter($orderShippingDetails, function ($a) { return null !== $a; }))) { + return []; + } + + return $orderShippingDetails; + } +} diff --git a/Gateway/EurekaPaymentGateway.php b/Gateway/EurekaPaymentGateway.php new file mode 100644 index 0000000..5c14b11 --- /dev/null +++ b/Gateway/EurekaPaymentGateway.php @@ -0,0 +1,375 @@ +eurekaPaymentGatewayClient = $eurekaPaymentGatewayClient; + } + + private function buildOptions( + PaymentGatewayConfigurationInterface $paymentGatewayConfiguration, + Transaction $transaction + ): array { + foreach ($this->getRequiredTransactionMetadata() as $requiredTransactionMetadata) { + if (!$transaction->hasMetadata($requiredTransactionMetadata)) { + throw new \UnexpectedValueException( + sprintf('The transaction metadata "%s" must be set', $requiredTransactionMetadata) + ); + } + } + + return [ + 'version' => $paymentGatewayConfiguration->get('version'), + 'merchantID' => $paymentGatewayConfiguration->get('merchant_id'), + 'merchantSiteID' => $paymentGatewayConfiguration->get('merchant_site_id'), + 'secretKey' => $paymentGatewayConfiguration->get('secret_key'), + 'paymentOptionRef' => $paymentGatewayConfiguration->get('payment_option_reference'), + 'orderRef' => $transaction->getId(), + 'decimalPosition' => 2, + 'currency' => $transaction->getCurrencyCode(), + 'country' => $transaction->getMetadata('Customer.Country'), + 'customerRef' => $transaction->getCustomerId(), + 'date' => (new \DateTime('now'))->format('Ymd'), + 'amount' => $transaction->getAmount(), + 'merchantHomeUrl' => $paymentGatewayConfiguration->get('return_url'), + 'merchantReturnUrl' => $paymentGatewayConfiguration->get('return_url'), + 'merchantNotifyUrl' => $paymentGatewayConfiguration->get('callback_url'), + 'scoringToken' => $this->requestScoringToken( + $paymentGatewayConfiguration, + $transaction + ), + ]; + } + + private function requestScoringToken( + PaymentGatewayConfigurationInterface $paymentGatewayConfiguration, + Transaction $transaction + ) { + $scoringToken = $this->eurekaPaymentGatewayClient->getScoringToken( + $paymentGatewayConfiguration->get('score_type'), + [ + 'Header' => [ + 'Context' => [ + 'MerchantId' => $paymentGatewayConfiguration->get('merchant_id'), + 'MerchantSiteId' => $paymentGatewayConfiguration->get('merchant_site_id'), + ], + 'Localization' => [ + 'Country' => $transaction->getMetadata('Customer.Country'), + 'Currency' => $transaction->getCurrencyCode(), + 'DecimalPosition' => $transaction->getMetadata('Order.DecimalPosition'), + 'Language' => $transaction->getMetadata('Customer.Country'), + ], + 'SecurityContext' => [ + 'TokenId' => $this->eurekaPaymentGatewayClient->getSTSToken( + $paymentGatewayConfiguration->get('username'), + $paymentGatewayConfiguration->get('password') + ), + ], + 'Version' => $paymentGatewayConfiguration->get('version'), + ], + 'Request' => [ + 'Customer' => [ + 'CustomerRef' => $transaction->getCustomerId(), + 'LastName' => $transaction->getMetadata('Customer.LastName'), + 'FirstName' => $transaction->getMetadata('Customer.FirstName'), + 'Civility' => $transaction->getMetadata('Customer.Civility'), + 'MaidenName' => $transaction->getMetadata('Customer.MaidenName'), + 'BirthDate' => $transaction->getMetadata('Customer.BirthDate'), + 'BirthZipCode' => $transaction->getMetadata('Customer.BirthZipCode'), + 'PhoneNumber' => $transaction->getMetadata('Customer.PhoneNumber'), + 'CellPhoneNumber' => $transaction->getMetadata('Customer.CellPhoneNumber'), + 'Email' => $transaction->getCustomerEmail(), + 'Address1' => $transaction->getMetadata('Customer.Address1'), + 'Address2' => $transaction->getMetadata('Customer.Address2'), + 'Address3' => $transaction->getMetadata('Customer.Address3'), + 'Address4' => $transaction->getMetadata('Customer.Address4'), + 'ZipCode' => $transaction->getMetadata('Customer.ZipCode'), + 'City' => $transaction->getMetadata('Customer.City'), + 'Country' => $transaction->getMetadata('Customer.Country'), + 'Nationality' => $transaction->getMetadata('Customer.Nationality'), + 'IpAddress' => $transaction->getMetadata('Customer.IpAddress'), + 'WhiteList' => $transaction->getMetadata('Customer.WhiteList'), + ], + 'Order' => [ + 'OrderDate' => (new \DateTime('now'))->format('Y-m-d'), + 'SaleChannel' => $transaction->getMetadata('Order.SaleChannel'), + 'ShippingMethod' => $transaction->getMetadata('Order.ShippingMethod'), + 'ShoppingCartItemCount' => $transaction->getMetadata('Order.ShoppingCartItemCount'), + 'ShoppingCartRef' => $transaction->getId(), + 'TotalAmount' => $transaction->getAmount(), + ], + 'OptionalCustomerHistory' => [ + 'CanceledOrderAmount' => $transaction->getMetadata('OptionalCustomerHistory.CanceledOrderAmount'), + 'CanceledOrderCount' => $transaction->getMetadata('OptionalCustomerHistory.CanceledOrderCount'), + 'FirstOrderDate' => $transaction->getMetadata('OptionalCustomerHistory.FirstOrderDate'), + 'FraudAlertCount' => $transaction->getMetadata('OptionalCustomerHistory.FraudAlertCount'), + 'LastOrderDate' => $transaction->getMetadata('OptionalCustomerHistory.LastOrderDate'), + 'PaymentIncidentCount' => $transaction->getMetadata('OptionalCustomerHistory.PaymentIncidentCount'), + 'RefusedManyTimesOrderCount' => $transaction->getMetadata('OptionalCustomerHistory.RefusedManyTimesOrderCount'), + 'UnvalidatedOrderCount' => $transaction->getMetadata('OptionalCustomerHistory.UnvalidatedOrderCount'), + 'ValidatedOneTimeOrderCount' => $transaction->getMetadata('OptionalCustomerHistory.ValidatedOneTimeOrderCount'), + 'ValidatedOrderCount' => $transaction->getMetadata('OptionalCustomerHistory.ValidatedOrderCount'), + 'ClientIpAddressRecurrence' => $transaction->getMetadata('OptionalCustomerHistory.ClientIpAddressRecurrence'), + 'OngoingLitigationOrderAmount' => $transaction->getMetadata('OptionalCustomerHistory.OngoingLitigationOrderAmount'), + 'PaidLitigationOrderAmount24Month' => $transaction->getMetadata('OptionalCustomerHistory.PaidLitigationOrderAmount24Months'), + 'ScoreSimulationCount7Days' => $transaction->getMetadata('OptionalCustomerHistory.ScoreSimulationCount7Days'), + ], + 'OptionalTravelDetails' => [ + 'Insurance' => $transaction->getMetadata('OptionalTravelDetails.Insurance'), + 'Type' => $transaction->getMetadata('OptionalTravelDetails.Type'), + 'DepartureDate' => $transaction->getMetadata('OptionalTravelDetails.DepartureDate'), + 'ReturnDate' => $transaction->getMetadata('OptionalTravelDetails.ReturnDate'), + 'DestinationCountry' => $transaction->getMetadata('OptionalTravelDetails.DestinationCountry'), + 'TicketCount' => $transaction->getMetadata('OptionalTravelDetails.TicketCount'), + 'TravellerCount' => $transaction->getMetadata('OptionalTravelDetails.TravellerCount'), + 'Class' => $transaction->getMetadata('OptionalTravelDetails.Class'), + 'OwnTicket' => $transaction->getMetadata('OptionalTravelDetails.OwnTicket'), + 'MainDepartureCompany' => $transaction->getMetadata('OptionalTravelDetails.MainDepartureCompany'), + 'DepartureAirport' => $transaction->getMetadata('OptionalTravelDetails.DepartureAirport'), + 'ArrivalAirport' => $transaction->getMetadata('OptionalTravelDetails.ArrivalAirport'), + 'DiscountCode' => $transaction->getMetadata('OptionalTravelDetails.DiscountCode'), + 'LuggageSupplement' => $transaction->getMetadata('OptionalTravelDetails.LuggageSupplement'), + 'ModificationAnnulation' => $transaction->getMetadata('OptionalTravelDetails.ModificationAnnulation'), + 'TravellerPassportList' => $transaction->getMetadata('OptionalTravelDetails.TravellerPassportList'), + ], + 'OptionalStayDetails' => [ + 'Company' => $transaction->getMetadata('OptionalStayDetails.Company'), + 'Destination' => $transaction->getMetadata('OptionalStayDetails.Destination'), + 'NightNumber' => $transaction->getMetadata('OptionalStayDetails.NightNumber'), + 'RoomRange' => $transaction->getMetadata('OptionalStayDetails.RoomRange'), + ], + 'OptionalProductDetails' => [ + 'Categorie1' => $transaction->getMetadata('OptionalProductDetails.Categorie1'), + 'Categorie2' => $transaction->getMetadata('OptionalProductDetails.Categorie2'), + 'Categorie3' => $transaction->getMetadata('OptionalProductDetails.Categorie3'), + ], + 'OptionalPreScoreInformation' => [ + 'RequestID' => $transaction->getMetadata('PreScoreInformation.RequestID'), + ], + 'AdditionalNumericFieldList' => $transaction->getMetadata('AdditionalNumericFieldList'), + 'AdditionalTextFieldList' => $transaction->getMetadata('AdditionalTextFieldList'), + 'OptionalShippingDetails' => [ + 'ShippingAdress1' => $transaction->getMetadata('OptionalShippingDetails.ShippingAdress1'), + 'ShippingAdress2' => $transaction->getMetadata('OptionalShippingDetails.ShippingAdress2'), + 'ShippingAdressCity' => $transaction->getMetadata('OptionalShippingDetails.ShippingAdressCity'), + 'ShippingAdressZip' => $transaction->getMetadata('OptionalShippingDetails.ShippingAdressZip'), + 'ShippingAdressCountry' => $transaction->getMetadata('OptionalShippingDetails.ShippingAdressCountry'), + ], + ], + ] + ); + + $transaction->addMetadata('scoring_token', $scoringToken); + + $this->dispatcher->dispatch(new TransactionEvent($transaction), TransactionEvent::UPDATED); + + return $scoringToken; + } + + private function buildHmac(array $options, string $hmacType): string + { + $hmacData = ''; + + foreach ($this->getHmacBuildParameters($hmacType) as $parameterName) { + if (1 < count($parameterNames = explode('|', $parameterName))) { + $i = 0; + while (isset($options[sprintf('%s%s', $parameterNames[0], ++$i)])) { + for ($j = 0; $j < count($parameterNames); ++$j) { + $hmacData = sprintf('%s*%s', $hmacData, $options[sprintf('%s%s', $parameterNames[$j], $i)]); + } + } + + continue; + } + + $realParameterName = '?' !== $parameterName[0] ? $parameterName : substr($parameterName, 1); + if ('?' !== $parameterName[0] || isset($options[$realParameterName])) { + $hmacData = sprintf('%s*%s', $hmacData, isset($options[$realParameterName]) ? $options[$realParameterName] : ''); + } + } + + return hash_hmac('sha1', utf8_encode(sprintf('%s*', substr($hmacData, 1))), utf8_encode($options['secretKey'])); + } + + public function initialize( + PaymentGatewayConfigurationInterface $paymentGatewayConfiguration, + Transaction $transaction + ): array { + $options = $this->buildOptions($paymentGatewayConfiguration, $transaction); + + return array_merge($options, [ + 'hmac' => $this->buildHmac($options, self::HMAC_TYPE_ENTRY), + ]); + } + + public function buildHTMLView( + PaymentGatewayConfigurationInterface $paymentGatewayConfiguration, + Transaction $transaction + ): string { + $initializationData = $this->initialize($paymentGatewayConfiguration, $transaction); + + return $this->templating->render('@IDCIPayment/Gateway/eureka.html.twig', [ + 'url' => $this->eurekaPaymentGatewayClient->getPaymentFormUrl(), + 'initializationData' => $initializationData, + ]); + } + + public function getResponse( + Request $request, + PaymentGatewayConfigurationInterface $paymentGatewayConfiguration + ): GatewayResponse { + if (!$request->isMethod('POST')) { + throw new \UnexpectedValueException('Eureka : Payment Gateway error (Request method should be POST)'); + } + + $gatewayResponse = (new GatewayResponse()) + ->setDate(new \DateTime()) + ->setStatus(PaymentStatus::STATUS_FAILED) + ->setRaw($request->request->all()) + ; + + if (!$request->request->has('hmac')) { + return $gatewayResponse->setMessage('The request do not contains "hmac"'); + } + + $hmac = $this->buildHmac( + array_merge($request->request->all(), ['secretKey' => $paymentGatewayConfiguration->get('secret_key')]), + self::HMAC_TYPE_OUT + ); + + if (strtolower($request->request->get('hmac')) !== strtolower($hmac)) { + return $gatewayResponse->setMessage('Hmac check failed'); + } + + $gatewayResponse + ->setTransactionUuid($request->request->get('orderRef')) + ->setAmount($request->request->get('amount')) + ->setCurrencyCode($request->request->get('currency')) + ; + + if ('0' !== $request->request->get('returnCode')) { + $gatewayResponse->setMessage(EurekaStatusCode::getStatusMessage($returnParams['responseCode'])); + + if ('6' === $returnParams['responseCode']) { + $gatewayResponse->setStatus(PaymentStatus::STATUS_CANCELED); + } + + return $gatewayResponse; + } + + return $gatewayResponse->setStatus(PaymentStatus::STATUS_APPROVED); + } + + public static function getParameterNames(): ?array + { + return array_merge( + parent::getParameterNames(), + [ + 'version', + 'merchant_id', + 'merchant_site_id', + 'payment_option_reference', + 'score_type', + 'username', + 'password', + 'secret_key', + ] + ); + } + + private function getHmacBuildParameters(string $hmacType): array + { + if (self::HMAC_TYPE_OUT === $hmacType) { + return [ + 'version', + 'merchantID', + 'merchantSiteID', + 'paymentOptionRef', + 'orderRef', + 'freeText', + 'decimalPosition', + 'currency', + 'country', + 'invoiceID', + 'customerRef', + 'date', + 'amount', + 'returnCode', + 'merchantAccountRef', + 'scheduleDate|scheduleAmount', + ]; + } + + return [ + 'version', + 'merchantID', + 'merchantSiteID', + 'paymentOptionRef', + 'orderRef', + 'freeText', + 'decimalPosition', + 'currency', + 'country', + 'invoiceID', + 'customerRef', + 'date', + 'amount', + 'orderRowsAmount', + 'orderFeesAmount', + 'orderDiscountAmount', + 'orderShippingCost', + '?allowCardStorage', + '?passwordRequired', + '?merchantAuthenticateUrl', + 'storedCardID|storedCardLabel', + 'merchantHomeUrl', + 'merchantBackUrl', + 'merchantReturnUrl', + 'merchantNotifyUrl', + ]; + } + + private function getRequiredTransactionMetadata(): array + { + return [ + 'Customer.LastName', + 'Customer.FirstName', + 'Customer.Civility', + 'Customer.BirthDate', + 'Customer.BirthZipCode', + 'Customer.PhoneNumber', + 'Customer.Address1', + 'Customer.ZipCode', + 'Customer.City', + 'Customer.Country', + 'Order.SaleChannel', + 'Order.ShippingMethod', + ]; + } +} diff --git a/Gateway/PayboxPaymentGateway.php b/Gateway/PayboxPaymentGateway.php index 6b28011..d2a6f07 100644 --- a/Gateway/PayboxPaymentGateway.php +++ b/Gateway/PayboxPaymentGateway.php @@ -9,6 +9,7 @@ use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; use Payum\ISO4217\ISO4217; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class PayboxPaymentGateway extends AbstractPaymentGateway { @@ -19,11 +20,12 @@ class PayboxPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $serverHostName, string $keyPath, string $publicKeyUrl ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->serverHostName = $serverHostName; $this->keyPath = $keyPath; diff --git a/Gateway/SofincoPaymentGateway.php b/Gateway/SofincoPaymentGateway.php index df27848..e1a2645 100644 --- a/Gateway/SofincoPaymentGateway.php +++ b/Gateway/SofincoPaymentGateway.php @@ -8,6 +8,7 @@ use IDCI\Bundle\PaymentBundle\Model\Transaction; use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class SofincoPaymentGateway extends AbstractPaymentGateway { @@ -24,9 +25,10 @@ class SofincoPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $serverUrl ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->serverUrl = $serverUrl; } diff --git a/Gateway/StatusCode/EurekaStatusCode.php b/Gateway/StatusCode/EurekaStatusCode.php new file mode 100644 index 0000000..1d324b9 --- /dev/null +++ b/Gateway/StatusCode/EurekaStatusCode.php @@ -0,0 +1,20 @@ + 'Refused', + '2' => 'Refused by bank', + '3' => 'Technical error', + '4' => 'Pending', + '5' => 'Unknown', + '6' => 'Canceled', + ]; + + public static function getStatusMessage(string $code) + { + return self::STATUS[$code]; + } +} diff --git a/Gateway/StripePaymentGateway.php b/Gateway/StripePaymentGateway.php index 4f27d1c..c6ef0fb 100644 --- a/Gateway/StripePaymentGateway.php +++ b/Gateway/StripePaymentGateway.php @@ -6,9 +6,9 @@ use IDCI\Bundle\PaymentBundle\Model\PaymentGatewayConfigurationInterface; use IDCI\Bundle\PaymentBundle\Model\Transaction; use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; -use Stripe; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class StripePaymentGateway extends AbstractPaymentGateway { @@ -17,9 +17,12 @@ class StripePaymentGateway extends AbstractPaymentGateway */ private $router; - public function __construct(\Twig_Environment $templating, UrlGeneratorInterface $router) - { - parent::__construct($templating); + public function __construct( + \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, + UrlGeneratorInterface $router + ) { + parent::__construct($templating, $dispatcher); $this->router = $router; } diff --git a/Gateway/SystemPayPaymentGateway.php b/Gateway/SystemPayPaymentGateway.php index 63acec4..cf246e2 100644 --- a/Gateway/SystemPayPaymentGateway.php +++ b/Gateway/SystemPayPaymentGateway.php @@ -10,6 +10,7 @@ use IDCI\Bundle\PaymentBundle\Payment\PaymentStatus; use Payum\ISO4217\ISO4217; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class SystemPayPaymentGateway extends AbstractPaymentGateway { @@ -24,9 +25,10 @@ class SystemPayPaymentGateway extends AbstractPaymentGateway public function __construct( \Twig_Environment $templating, + EventDispatcherInterface $dispatcher, string $serverUrl ) { - parent::__construct($templating); + parent::__construct($templating, $dispatcher); $this->serverUrl = $serverUrl; } diff --git a/Resources/config/config.yml b/Resources/config/config.yml index 816980a..49f60e5 100644 --- a/Resources/config/config.yml +++ b/Resources/config/config.yml @@ -29,5 +29,8 @@ parameters: # Sofinco default server url for test purposes idci_payment.sofinco.server_url: https://re7financement.transcred.com/sofgate.asp # prod ? + # Eureka default server url for test purposes + idci_payment.eureka.server_host_name: recette-cb4x.fr # prod: cb4x.fr + monolog: channels: [payment] diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 5958173..f04a1b3 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -135,3 +135,15 @@ services: $serverUrl: '%idci_payment.sofinco.server_url%' tags: - { name: idci_payment.gateways, alias: sofinco } + + IDCI\Bundle\PaymentBundle\Gateway\EurekaPaymentGateway: + tags: + - { name: idci_payment.gateways, alias: eureka } + + # Payment Gateway Clients + + IDCI\Bundle\PaymentBundle\Gateway\Client\EurekaPaymentGatewayClient: + arguments: + $serverHostName: '%idci_payment.eureka.server_host_name%' + calls: + - [setCache, ['@?cache.idci_payment']] diff --git a/Resources/views/Gateway/eureka.html.twig b/Resources/views/Gateway/eureka.html.twig new file mode 100644 index 0000000..6ab0b47 --- /dev/null +++ b/Resources/views/Gateway/eureka.html.twig @@ -0,0 +1,8 @@ +
+ {% for fieldName, fieldValue in initializationData %} + {% if fieldName != 'secretKey' %} + + {% endif %} + {% endfor %} + +
diff --git a/Resources/views/Gateway/eureka/pay_order_rank.xml.twig b/Resources/views/Gateway/eureka/pay_order_rank.xml.twig new file mode 100644 index 0000000..40f2ef3 --- /dev/null +++ b/Resources/views/Gateway/eureka/pay_order_rank.xml.twig @@ -0,0 +1,37 @@ + + + + + + + {{ Header.Context.MerchantId }} + {{ Header.Context.MerchantSiteId }} + + + {{ Header.Localization.Country }} + {{ Header.Localization.Currency }} + {{ Header.Localization.DecimalPosition }} + {{ Header.Localization.Language }} + + + + + + + {{ Header.SecurityContext.TokenId }} + + + {{ Header.Version }} + + + {{ PayOrderRankRequestMessage.Amount }} + {{ PayOrderRankRequestMessage.Attempt }} + {{ PayOrderRankRequestMessage.OrderRef }} + {{ PayOrderRankRequestMessage.Rank }} + + + + diff --git a/Resources/views/Gateway/eureka/score.xml.twig b/Resources/views/Gateway/eureka/score.xml.twig new file mode 100644 index 0000000..c227129 --- /dev/null +++ b/Resources/views/Gateway/eureka/score.xml.twig @@ -0,0 +1,153 @@ +{% macro display_field(name, value) %} + {% if value is not null and value is not empty %}{{ value }}{% else %}{% endif %} +{% endmacro %} + + + + + + + {{ Header.Context.MerchantId }} + {{ Header.Context.MerchantSiteId }} + + + {{ Header.Localization.Country }} + {{ Header.Localization.Currency }} + {{ Header.Localization.DecimalPosition }} + {{ Header.Localization.Language }} + + + + + + + {{ Header.SecurityContext.TokenId }} + + + {{ Header.Version }} + + + + {{ _self.display_field('Address1', Request.Customer.Address1) }} + {{ _self.display_field('Address2', Request.Customer.Address2) }} + {{ _self.display_field('Address3', Request.Customer.Address3) }} + {{ _self.display_field('Address4', Request.Customer.Address4) }} + {{ _self.display_field('BirthDate', Request.Customer.BirthDate) }} + {{ _self.display_field('BirthZipCode', Request.Customer.BirthZipCode) }} + {{ _self.display_field('CellPhoneNumber', Request.Customer.CellPhoneNumber) }} + {{ _self.display_field('City', Request.Customer.City) }} + {{ _self.display_field('Civility', Request.Customer.Civility) }} + {{ _self.display_field('Country', Request.Customer.Country) }} + {{ _self.display_field('CustomerRef', Request.Customer.CustomerRef) }} + {{ _self.display_field('Email', Request.Customer.Email) }} + {{ _self.display_field('FirstName', Request.Customer.FirstName) }} + {{ _self.display_field('LastName', Request.Customer.LastName) }} + {{ _self.display_field('MaidenName', Request.Customer.MaidenName) }} + {{ _self.display_field('PhoneNumber', Request.Customer.PhoneNumber) }} + {{ _self.display_field('ZipCode', Request.Customer.ZipCode) }} + {{ _self.display_field('Nationality', Request.Customer.Nationality) }} + {{ _self.display_field('IpAddress', Request.Customer.IpAddress) }} + {{ _self.display_field('WhiteList', Request.Customer.WhiteList) }} + + + {{ _self.display_field('OrderDate', Request.Order.OrderDate) }} + {{ _self.display_field('SaleChannel', Request.Order.SaleChannel) }} + {{ _self.display_field('ShippingMethod', Request.Order.ShippingMethod) }} + {{ _self.display_field('ShoppingCartItemCount', Request.Order.ShoppingCartItemCount) }} + {{ _self.display_field('ShoppingCartRef', Request.Order.ShoppingCartRef) }} + {{ _self.display_field('TotalAmount', Request.Order.TotalAmount) }} + + {% if Request.OptionalCustomerHistory is not empty %} + + {{ _self.display_field('CanceledOrderAmount', Request.OptionalCustomerHistory.CanceledOrderAmount) }} + {{ _self.display_field('CanceledOrderCount', Request.OptionalCustomerHistory.CanceledOrderCount) }} + {{ _self.display_field('FirstOrderDate', Request.OptionalCustomerHistory.FirstOrderDate) }} + {{ _self.display_field('FraudAlertCount', Request.OptionalCustomerHistory.FraudAlertCount) }} + {{ _self.display_field('LastOrderDate', Request.OptionalCustomerHistory.LastOrderDate) }} + {{ _self.display_field('PaymentIncidentCount', Request.OptionalCustomerHistory.PaymentIncidentCount) }} + {{ _self.display_field('RefusedManyTimesOrderCount', Request.OptionalCustomerHistory.RefusedManyTimesOrderCount) }} + {{ _self.display_field('ValidatedOneTimeOrderCount', Request.OptionalCustomerHistory.ValidatedOneTimeOrderCount) }} + {{ _self.display_field('ValidatedOrderCount', Request.OptionalCustomerHistory.ValidatedOrderCount) }} + {{ _self.display_field('ClientIpAddressRecurrence', Request.OptionalCustomerHistory.ClientIpAddressRecurrence) }} + {{ _self.display_field('OngoingLitigationOrderAmount', Request.OptionalCustomerHistory.OngoingLitigationOrderAmount) }} + {{ _self.display_field('PaidLitigationOrderAmount24Month', Request.OptionalCustomerHistory.PaidLitigationOrderAmount24Month) }} + {{ _self.display_field('ScoreSimulationCount7Days', Request.OptionalCustomerHistory.ScoreSimulationCount7Days) }} + + {% endif %} + {% if Request.OptionalTravelDetails is not empty %} + + {{ _self.display_field('ArrivalAirport', Request.OptionalTravelDetails.ArrivalAirport) }} + {{ _self.display_field('Class', Request.OptionalTravelDetails.Class) }} + {{ _self.display_field('DepartureAirport', Request.OptionalTravelDetails.DepartureAirport) }} + {{ _self.display_field('DepartureDate', Request.OptionalTravelDetails.DepartureDate) }} + {{ _self.display_field('DestinationCountry', Request.OptionalTravelDetails.DestinationCountry) }} + {{ _self.display_field('Insurance', Request.OptionalTravelDetails.Insurance) }} + {{ _self.display_field('MainDepartureCompany', Request.OptionalTravelDetails.MainDepartureCompany) }} + {{ _self.display_field('OwnTicket', Request.OptionalTravelDetails.OwnTicket) }} + {{ _self.display_field('ReturnDate', Request.OptionalTravelDetails.ReturnDate) }} + {{ _self.display_field('TicketCount', Request.OptionalTravelDetails.TicketCount) }} + {{ _self.display_field('TravellerCount', Request.OptionalTravelDetails.TravellerCount) }} + {{ _self.display_field('TravellerPassportList', Request.OptionalTravelDetails.TravellerPassportList) }} + {{ _self.display_field('Type', Request.OptionalTravelDetails.Type) }} + {{ _self.display_field('DiscountCode', Request.OptionalTravelDetails.DiscountCode) }} + {{ _self.display_field('LuggageSupplement', Request.OptionalTravelDetails.LuggageSupplement) }} + {{ _self.display_field('ModificationAnnulation', Request.OptionalTravelDetails.ModificationAnnulation) }} + + {% endif %} + {% if Request.OptionalStayDetails is not empty %} + + {{ _self.display_field('Company', Request.OptionalStayDetails.Company) }} + {{ _self.display_field('Destination', Request.OptionalStayDetails.Destination) }} + {{ _self.display_field('NightNumber', Request.OptionalStayDetails.NightNumber) }} + {{ _self.display_field('RoomRange', Request.OptionalStayDetails.RoomRange) }} + + {% endif %} + {% if Request.OptionalProductDetails is not empty %} + + {{ _self.display_field('Categorie1', Request.OptionalProductDetails.Categorie1) }} + {{ _self.display_field('Categorie2', Request.OptionalProductDetails.Categorie2) }} + {{ _self.display_field('Categorie3', Request.OptionalProductDetails.Categorie3) }} + + {% endif %} + {% if Request.OptionalPreScoreInformation is not empty %} + + {{ _self.display_field('RequestID', Request.OptionalPreScoreInformation.RequestID) }} + + {% endif %} + {% if Request.AdditionalNumericFieldList is not empty %} + + {% for AdditionalNumericField in Request.AdditionalNumericFieldList %} + + {{ _self.display_field('Index', AdditionalNumericField.Index) }} + {{ _self.display_field('Value', AdditionalNumericField.Value) }} + + {% endfor %} + + {% endif %} + {% if Request.AdditionalTextFieldList is not empty %} + + {% for AdditionalTextField in Request.AdditionalTextFieldList %} + + {{ _self.display_field('Index', AdditionalTextField.Index) }} + {{ _self.display_field('Value', AdditionalTextField.Value) }} + + {% endfor %} + + {% endif %} + {% if Request.OptionalPreScoreInformation is not empty %} + + {{ _self.display_field('Adress1', Request.OptionalPreScoreInformation.Adress1) }} + {{ _self.display_field('Adress2', Request.OptionalPreScoreInformation.Adress2) }} + {{ _self.display_field('AdressCity', Request.OptionalPreScoreInformation.AdressCity) }} + {{ _self.display_field('AdressCountry', Request.OptionalPreScoreInformation.AdressCountry) }} + {{ _self.display_field('AdressZip', Request.OptionalPreScoreInformation.AdressZip) }} + + {% endif %} + + + + diff --git a/Resources/views/Gateway/eureka/sts_token.xml.twig b/Resources/views/Gateway/eureka/sts_token.xml.twig new file mode 100644 index 0000000..0536f5f --- /dev/null +++ b/Resources/views/Gateway/eureka/sts_token.xml.twig @@ -0,0 +1,14 @@ + + + + + {{ username }} + {{ password }} + + {{ merchant_url }} + + + + diff --git a/Resources/views/Gateway/eureka/update_order.xml.twig b/Resources/views/Gateway/eureka/update_order.xml.twig new file mode 100644 index 0000000..65bd61e --- /dev/null +++ b/Resources/views/Gateway/eureka/update_order.xml.twig @@ -0,0 +1,36 @@ + + + + + + + {{ Header.Context.MerchantId }} + {{ Header.Context.MerchantSiteId }} + + + {{ Header.Localization.Country }} + {{ Header.Localization.Currency }} + {{ Header.Localization.DecimalPosition }} + {{ Header.Localization.Language }} + + + + + + + {{ Header.SecurityContext.TokenId }} + + + {{ Header.Version }} + + + {{ UpdateOrderRequestMessage.NewAmount }} + {{ UpdateOrderRequestMessage.OrderRef }} + {{ UpdateOrderRequestMessage.ScoringToken }} + + + + diff --git a/composer.json b/composer.json index 2488e4e..ca29686 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "require": { "php": ">=7.0", "symfony/dependency-injection": "^4.0", + "symfony/dom-crawler": "^4.0", "symfony/framework-bundle": "^4.0", "symfony/form": "^4.0", "symfony/templating": "^4.0", @@ -31,7 +32,8 @@ "pascaldevink/shortuuid": "~2.0" }, "suggest": { - "idci/step-bundle": "~3.0" + "idci/step-bundle": "~3.0", + "symfony/cache": "^4.0" }, "require-dev": { "phpunit/phpunit": "~5.7"