diff --git a/src/Model/Request/SecciRequest.php b/src/Model/Request/SecciRequest.php new file mode 100644 index 0000000..b9932d0 --- /dev/null +++ b/src/Model/Request/SecciRequest.php @@ -0,0 +1,31 @@ +getHead()->admittedFields['TransactionId'])) { + $this->setErrorMsg('Secci Request does not allow transaction id'); + + return false; + } + + return true; + } +} diff --git a/src/Model/Request/SubModel/Content.php b/src/Model/Request/SubModel/Content.php index 53a2415..3628001 100644 --- a/src/Model/Request/SubModel/Content.php +++ b/src/Model/Request/SubModel/Content.php @@ -14,6 +14,7 @@ use RatePAY\Model\Request\SubModel\Content\InstallmentCalculation; use RatePAY\Model\Request\SubModel\Content\Invoicing; use RatePAY\Model\Request\SubModel\Content\Payment; +use RatePAY\Model\Request\SubModel\Content\Secci; use RatePAY\Model\Request\SubModel\Content\ShoppingBasket; /** @@ -29,6 +30,8 @@ * @method InstallmentCalculation getInstallmentCalculation() * @method $this setAdditional(Additional $additional) * @method Additional getAdditional() + * @method $this setSecci(Secci $secci) + * @method Secci getSecci() */ class Content extends AbstractModel { @@ -75,5 +78,9 @@ class Content extends AbstractModel 'mandatory' => false, 'instanceOf' => 'Content\\Additional', ], + 'Secci' => [ + 'mandatory' => false, + 'instanceOf' => 'Content\\Secci', + ], ]; } diff --git a/src/Model/Request/SubModel/Content/Payment/InstallmentDetails.php b/src/Model/Request/SubModel/Content/Payment/InstallmentDetails.php index a7c8423..38a80ab 100644 --- a/src/Model/Request/SubModel/Content/Payment/InstallmentDetails.php +++ b/src/Model/Request/SubModel/Content/Payment/InstallmentDetails.php @@ -12,16 +12,16 @@ use RatePAY\Model\Request\SubModel\AbstractModel; /** - * @method $this setInstallmentNumber(string $installmentNumber) - * @method string getInstallmentNumber() - * @method $this setInstallmentAmount(float $installmentAmount) - * @method float getInstallmentAmount() - * @method $this setLastInstallmentAmount(InstallmentDetails $lastInstallmentAmount) - * @method InstallmentDetails getLastInstallmentAmount() - * @method $this setInterestRate(string $interestRate) - * @method string getInterestRate() - * @method $this setPaymentFirstday(string $paymentFirstday) - * @method string getPaymentFirstday() + * @method $this setInstallmentNumber(string $installmentNumber) + * @method string getInstallmentNumber() + * @method $this setInstallmentAmount(float $installmentAmount) + * @method float getInstallmentAmount() + * @method $this setLastInstallmentAmount(float $lastInstallmentAmount) + * @method float getLastInstallmentAmount() + * @method $this setInterestRate(string $interestRate) + * @method string getInterestRate() + * @method $this setPaymentFirstday(string $paymentFirstday) + * @method string getPaymentFirstday() */ class InstallmentDetails extends AbstractModel { diff --git a/src/Model/Request/SubModel/Content/Secci.php b/src/Model/Request/SubModel/Content/Secci.php new file mode 100644 index 0000000..9acea02 --- /dev/null +++ b/src/Model/Request/SubModel/Content/Secci.php @@ -0,0 +1,149 @@ + [ + 'mandatory' => true, + 'uppercase' => true, + ], + 'Email' => [ + 'mandatoryByRule' => true, + ], + 'Action' => [ + 'mandatoryByRule' => true, + 'uppercase' => true, + ], + 'Language' => [ + 'mandatory' => false, + 'uppercase' => true, + ], + 'CountryCode' => [ + 'mandatory' => false, + 'uppercase' => true, + ], + ]; + + /** + * Secci rule : if DeliveryMethod is EMAIL, Email parameter must be given. if DeliveryMethod is PDF, Action parameter must be either PRINT or DOWNLOAD. + * + * @return bool + */ + protected function rule() + { + if ('EMAIL' == $this->admittedFields['DeliveryMethod']['value'] && + (!key_exists('value', $this->admittedFields['Email'])) + ) { + $this->setErrorMsg('email details missing'); + + return false; + } + + if ('PDF' == $this->admittedFields['DeliveryMethod']['value'] && + (!key_exists('value', $this->admittedFields['Action']) || !in_array(strtoupper($this->admittedFields['Action']['value']), ['PRINT', 'DOWNLOAD'])) + ) { + $this->setErrorMsg('action details missing or incorrect'); + + return false; + } + + return true; + } + + /** + * @param string $deliveryMethod + * + * @return self + * + * @throws \RatePAY\Exception\ModelException + */ + public function setDeliveryMethod($deliveryMethod) + { + return $this->__set('DeliveryMethod', $deliveryMethod); + } + + /** + * @param string $email + * + * @return self + * + * @throws \RatePAY\Exception\ModelException + */ + public function setEmail($email) + { + return $this->__set('Email', $email); + } + + /** + * @param string $action + * + * @return self + * + * @throws \RatePAY\Exception\ModelException + */ + public function setAction($action) + { + return $this->__set('Action', $action); + } + + /** + * @param string $language + * + * @return self + * + * @throws \RatePAY\Exception\ModelException + */ + public function setLanguage($language) + { + return $this->__set('Language', $language); + } + + /** + * @param string $countryCode + * + * @return self + * + * @throws \RatePAY\Exception\ModelException + */ + public function setCountryCode($countryCode) + { + return $this->__set('CountryCode', $countryCode); + } +} diff --git a/src/Model/Request/SubModel/Head.php b/src/Model/Request/SubModel/Head.php index bc48c0e..3d80196 100644 --- a/src/Model/Request/SubModel/Head.php +++ b/src/Model/Request/SubModel/Head.php @@ -23,6 +23,8 @@ * @method string getOperation() * @method $this setSubtype(string $subType) * @method string getSubtype() + * @method $this setAttestationToken(string $attestationToken) + * @method string getAttestationToken() * @method $this setCredential(Credential $credential) * @method Credential getCredential() * @method $this setExternal(External $external) @@ -83,6 +85,9 @@ class Head extends AbstractModel 'mandatory' => true, 'instanceOf' => 'Head\\Meta', ], + 'AttestationToken' => [ + 'mandatory' => false, + ], ]; /** diff --git a/src/Model/Response/SecciRequest.php b/src/Model/Response/SecciRequest.php new file mode 100644 index 0000000..f0f44eb --- /dev/null +++ b/src/Model/Response/SecciRequest.php @@ -0,0 +1,117 @@ +getStatusCode() == 'OK' && $this->getResultCode() == 504) { + if (!isset($this->getResponse()->content->{'secci-result'})) { + return; + } + $secciResult = $this->getResponse()->content->{'secci-result'}; + + $attestationToken = $this->getNodeValue($secciResult, 'attestation-token'); + $documentId = $this->getNodeValue($secciResult, 'document-id'); + if ($attestationToken === null || $documentId === null) { + return; + } + + $document = null; + $contentType = null; + if (isset($secciResult->{'document'})) { + $document = $this->getNodeValue($secciResult, 'document'); + $contentType = $this->getNodeValue($secciResult, 'content-type'); + // Document is base64 encoded and must be decodable + if ($document === null || $contentType === null || base64_decode($document, true) === false) { + return; + } + } + + $this->setResult(['attestationToken' => $attestationToken]); + $this->setResult(['documentId' => $documentId]); + if ($document !== null) { + $this->setResult(['document' => $document]); + $this->setResult(['contentType' => $contentType]); + } + $this->setSuccessful(); + } + } + + /** + * Returns the trimmed value of a child node or null if the node is missing or empty. + * + * @param \SimpleXMLElement $parent + * @param string $name + * + * @return string|null + */ + private function getNodeValue($parent, $name) + { + if (!isset($parent->{$name})) { + return null; + } + + $value = trim((string) $parent->{$name}); + + return ($value !== '') ? $value : null; + } + + /** + * Returns attestation token. + * + * @return string|null + */ + public function getAttestationToken() + { + return (key_exists('attestationToken', $this->result)) ? $this->result['attestationToken'] : null; + } + + /** + * Returns document id. + * + * @return string|null + */ + public function getDocumentId() + { + return (key_exists('documentId', $this->result)) ? $this->result['documentId'] : null; + } + + /** + * Returns document. + * + * @return string|null + */ + public function getDocument() + { + // Document variable comes as base64 encoded string! + if (!key_exists('document', $this->result)) { + return null; + } + + $document = base64_decode($this->result['document'], true); + + return ($document !== false) ? $document : null; + } + + /** + * Returns content type of the document. + * + * @return string|null + */ + public function getContentType() + { + return (key_exists('contentType', $this->result)) ? $this->result['contentType'] : null; + } +} diff --git a/src/Service/ModelMapper.php b/src/Service/ModelMapper.php index 2e79deb..3022871 100644 --- a/src/Service/ModelMapper.php +++ b/src/Service/ModelMapper.php @@ -23,6 +23,7 @@ class ModelMapper 'PaymentChange' => 'PaymentChange', 'PaymentConfirm' => 'PaymentConfirm', 'ProfileRequest' => 'ProfileRequest', + 'SecciRequest' => 'SecciRequest', ]; private static $subModelNamePathMapping = [ @@ -57,6 +58,7 @@ class ModelMapper 'Payment' => 'Content\\Payment', 'InstallmentDetails' => 'Content\\Payment\\InstallmentDetails', 'Invoicing' => 'Content\\Invoicing', + 'Secci' => 'Content\\Secci', ]; public static function getFullPathRequestModel($requestedModel) diff --git a/src/locales/de.php b/src/locales/de.php index 2deddef..5ade66a 100644 --- a/src/locales/de.php +++ b/src/locales/de.php @@ -83,4 +83,24 @@ 'wcd_sepa_terms_block_1' => 'Ich ermächtige die Ratepay GmbH von meinem Konto mittels Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die von der Ratepay GmbH auf mein Konto gezogenen Lastschriften einzulösen.', 'wcd_sepa_terms_block_2' => 'Ich kann innerhalb von acht Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen.', 'wcd_sepa_terms_block_3' => 'Es gelten dabei die mit dem Kreditinstitut vereinbarten Bedingungen.', + 'rp_secci_header' => 'EU-Standardinformationen für Verbraucherkredite', + 'rp_secci_choose_delivery_method' => 'Wählen Sie vor Abschluss Ihrer Bestellung, wie Sie diese Informationen erhalten möchten.', + 'rp_secci_learn_more' => 'Mehr erfahren', + 'rp_secci_description' => 'Die EU-Verbraucherkreditrichtlinien haben sich geändert und erfordern nun zusätzliche Informationen für Zahlung in 3 Raten und bestimmte Rechnungskäufe. Diese Informationen helfen Ihnen, die wesentlichen Kreditkonditionen vor Abschluss Ihres Kaufs zu verstehen. Unser Zahlungspartner Ratepay verwendet Ihre E-Mail-Adresse ausschließlich zum Versand dieser Informationen über einen externen E-Mail-Dienstleister.', + 'rp_secci_get_by_email' => 'Per E-Mail erhalten', + 'rp_secci_download_pdf' => 'PDF herunterladen', + 'rp_secci_choose_email' => 'Wir haben die erforderlichen EU-Standardinformationen für Verbraucherkredite gesendet an', + 'rp_secci_payment_methods' => 'Zahlungsart', + 'rp_secci_choose_payment_method' => 'Wenn Sie Rechnungskauf oder Zahle in 3 Raten wählen, werden diese Informationen automatisch an', + 'rp_secci_choose_payment_method2' => 'gesendet.', + 'rp_secci_change_email' => 'E-Mail ändern', + 'rp_secci_save' => 'Speichern', + 'rp_secci_method_invoice' => 'Ratepay Rechnung', + 'rp_secci_method_invoice_desc' => 'In 30 Tagen bezahlen.', + 'rp_secci_method_paypal_desc' => 'Sie werden zu PayPal weitergeleitet.', + 'rp_secci_method_cod' => 'Nachnahme', + 'rp_secci_method_cod_desc' => 'Zahlung bei Erhalt der Ware.', + 'rp_secci_method_installment' => 'Ratepay Zahle in 3 Raten', + 'rp_secci_method_installment_desc' => 'In 3 monatlichen Raten bezahlen.', + 'rp_secci_method_installment_desc2' => '(Ratenrechner hier)', ]; diff --git a/src/locales/en.php b/src/locales/en.php index 9f8e798..ab4c842 100644 --- a/src/locales/en.php +++ b/src/locales/en.php @@ -84,4 +84,24 @@ 'wcd_sepa_terms_block_1' => 'I hereby authorise Ratepay GmbH to collect payments from my account by direct debit. At the same time, I authorise my bank to debit my account in accordance with the instructions from Ratepay GmbH.', 'wcd_sepa_terms_block_2' => 'As part of my rights, I am entitled to a refund from my bank under the terms and conditions of my agreement with my bank. A refund must be claimed within 8 weeks starting from the date on which my account was debited. ', 'wcd_sepa_terms_block_3' => 'My rights are explained in a statement that I can obtain from my bank.', + 'rp_secci_header' => 'Standard EU Consumer Credit Information', + 'rp_secci_choose_delivery_method' => 'Before placing your order, choose how you’d like to receive this information.', + 'rp_secci_learn_more' => 'Learn more', + 'rp_secci_description' => 'EU consumer credit rules have changed and now require additional information for this payment method. This information helps you understand the key credit terms before completing your purchase. If you choose Get by email, our payment partner Ratepay will use your email address only to send this information, using a third-party email delivery provider.', + 'rp_secci_get_by_email' => 'Get by email', + 'rp_secci_download_pdf' => 'Download PDF', + 'rp_secci_choose_email' => "We've sent the required Standard European Consumer Credit Information to", + 'rp_secci_payment_methods' => 'Payment Method', + 'rp_secci_choose_payment_method' => 'Selecting Open invoice or installment purchase will automatically send this information to', + 'rp_secci_choose_payment_method2' => '.', + 'rp_secci_change_email' => 'Change email', + 'rp_secci_save' => 'Save', + 'rp_secci_method_invoice' => 'Ratepay Invoice', + 'rp_secci_method_invoice_desc' => 'Pay in 30 days.', + 'rp_secci_method_paypal_desc' => 'You will be redirected to PayPal.', + 'rp_secci_method_cod' => 'Cash on delivery', + 'rp_secci_method_cod_desc' => 'Payment upon receipt of the goods.', + 'rp_secci_method_installment' => 'Ratepay Installment Purchase', + 'rp_secci_method_installment_desc' => 'Pay in monthly installments.', + 'rp_secci_method_installment_desc2' => '(Installment calculator here)', ]; diff --git a/tests/Support/GatewayResponses.php b/tests/Support/GatewayResponses.php index dce3c83..df86682 100644 --- a/tests/Support/GatewayResponses.php +++ b/tests/Support/GatewayResponses.php @@ -157,6 +157,45 @@ public function getAutoConfirmationDeliveryResponse($params = []) return new Response($this->processXmlTemplate($xml, $bindings)); } + public function getSecciRequestResponse($params = []) + { + $bindings = array_merge( + [ + 'system-id' => 'FooSystem', + 'attestation-token' => 'foo-attestation-token', + 'document-id' => 'SCE2QIAE-10092026', + 'status-code' => 'OK', + 'reason-code' => '308', + 'result-code' => '504', + ], + $params + ); + + $xml = ' + + + {{ system-id }} + SECCI_REQUEST + SECCI_DOCUMENT + + + + + + + + + + + {{ attestation-token }} + {{ document-id }} + + + '; + + return new Response($this->processXmlTemplate($xml, $bindings)); + } + /** * @param string $template * @param array $bindings diff --git a/tests/Unit/Model/Request/SecciRequestTest.php b/tests/Unit/Model/Request/SecciRequestTest.php new file mode 100644 index 0000000..f78ac40 --- /dev/null +++ b/tests/Unit/Model/Request/SecciRequestTest.php @@ -0,0 +1,119 @@ +setHead($head->getModel()); + + $this->assertTrue($request->rule()); + } + + public function testRuleFailed() + { + $head = new ModelBuilder(); + $head->setTransactionId('foo-1234567890'); + + $request = new SecciRequest(); + $request->setHead($head->getModel()); + + $this->assertFalse($request->rule()); + $this->assertEquals('Secci Request does not allow transaction id', $request->getErrorMsg()); + } + + public function testArrayConversion() + { + $head = new ModelBuilder(); + $head->setArray([ + 'SystemId' => 'SuperSystem', + 'Operation' => 'SECCI_REQUEST', + 'Credential' => [ + 'ProfileId' => 'MY_AMAZING_PROFILE', + 'Securitycode' => 'super-secure-code', + ], + ]); + + $content = new ModelBuilder('Content'); + $content->setArray([ + 'Secci' => [ + 'DeliveryMethod' => 'EMAIL', + 'Email' => 'test@example.com', + ], + ]); + + $request = new SecciRequest(); + $request->setHead($head->getModel()); + $request->setContent($content->getModel()); + + $data = $request->toArray(); + + $this->assertTypeIsArray($data); + $this->assertArrayHasKey('head', $data); + $this->assertArrayHasKey('content', $data); + + $expected = print_r([ + 'secci' => [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + ], + ], true); + $current = print_r($data['content'], true); + $this->assertEquals($expected, $current); + } + + public function testArrayConversionFailsOnTransactionId() + { + $head = new ModelBuilder(); + $head->setArray([ + 'SystemId' => 'SuperSystem', + 'Operation' => 'SECCI_REQUEST', + 'TransactionId' => 'foo-1234567890', + 'Credential' => [ + 'ProfileId' => 'MY_AMAZING_PROFILE', + 'Securitycode' => 'super-secure-code', + ], + ]); + + $content = new ModelBuilder('Content'); + $content->setArray([ + 'Secci' => [ + 'DeliveryMethod' => 'EMAIL', + 'Email' => 'test@example.com', + ], + ]); + + $request = new SecciRequest(); + $request->setHead($head->getModel()); + $request->setContent($content->getModel()); + + $this->expectException(RuleSetException::class); + $this->expectExceptionMessage('Secci Request does not allow transaction id'); + + $request->toArray(); + } + + public function assertTypeIsArray($data) + { + if (method_exists($this, 'assertIsArray')) { + $this->assertIsArray($data); + } else { + $this->assertInternalType('array', $data); + } + } +} diff --git a/tests/Unit/Model/Request/SubModel/Content/PaymentTest.php b/tests/Unit/Model/Request/SubModel/Content/PaymentTest.php new file mode 100644 index 0000000..277022d --- /dev/null +++ b/tests/Unit/Model/Request/SubModel/Content/PaymentTest.php @@ -0,0 +1,56 @@ +setMethod('INVOICE') + ->setAmount(100.00); + + $array = $payment->toArray(); + + $expected = [ + 'attributes' => [ + 'method' => ['value' => 'INVOICE'], + ], + 'amount' => ['value' => 100.00], + ]; + + $this->assertEquals($expected, $array); + } + + public function testHandleByModelBuilder() + { + $builder = new ModelBuilder('Payment'); + $builder->setArray([ + 'Method' => 'INVOICE', + 'Amount' => 100.00, + ]); + + $array = $builder->getModel()->toArray(); + + $this->assertEquals('INVOICE', $array['attributes']['method']['value']); + $this->assertEquals(100.00, $array['amount']['value']); + } + + public function testCurrencyIsNotAdmitted() + { + // Currency is delivered by ShoppingBasket, not by Payment + $payment = new Payment(); + + $this->assertArrayNotHasKey('Currency', $payment->admittedFields); + } +} diff --git a/tests/Unit/Model/Request/SubModel/Content/SecciTest.php b/tests/Unit/Model/Request/SubModel/Content/SecciTest.php new file mode 100644 index 0000000..b19d37b --- /dev/null +++ b/tests/Unit/Model/Request/SubModel/Content/SecciTest.php @@ -0,0 +1,115 @@ +setDeliveryMethod('EMAIL') + ->setEmail('test@example.com'); + + $array = $secci->toArray(); + + $expected = [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + ]; + + $this->assertEquals($expected, $array); + } + + public function testToArrayWithAllFields() + { + $secci = (new Secci()) + ->setDeliveryMethod('EMAIL') + ->setEmail('test@example.com') + ->setAction('REQUEST') + ->setLanguage('DE') + ->setCountryCode('DE'); + + $array = $secci->toArray(); + + $expected = [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + 'action' => ['value' => 'REQUEST'], + 'language' => ['value' => 'DE'], + 'country-code' => ['value' => 'DE'], + ]; + + $this->assertEquals($expected, $array); + } + + public function testSetterReturnsModel() + { + $secci = new Secci(); + + $this->assertSame($secci, $secci->setDeliveryMethod('EMAIL')); + } + + public function testGetters() + { + $secci = (new Secci()) + ->setDeliveryMethod('EMAIL') + ->setEmail('test@example.com') + ->setAction('REQUEST') + ->setLanguage('DE') + ->setCountryCode('DE'); + + $this->assertEquals('EMAIL', $secci->getDeliveryMethod()); + $this->assertEquals('test@example.com', $secci->getEmail()); + $this->assertEquals('REQUEST', $secci->getAction()); + $this->assertEquals('DE', $secci->getLanguage()); + $this->assertEquals('DE', $secci->getCountryCode()); + } + + public function testGetterReturnsNullIfNotSet() + { + $secci = new Secci(); + + $this->assertNull($secci->getEmail()); + } + + public function testThrowErrorIfNoDeliveryMethod() + { + $secci = new Secci(); + + $this->expectException(ModelException::class); + $this->expectExceptionMessage('Model exception : Field \'DeliveryMethod\' is required'); + + $secci->toArray(); + } + + public function testBuildByModelBuilder() + { + $builder = new ModelBuilder('Secci'); + $builder->setArray([ + 'DeliveryMethod' => 'EMAIL', + 'Email' => 'test@example.com', + ]); + + $secci = $builder->getModel(); + + $this->assertInstanceOf(Secci::class, $secci); + + $expected = [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + ]; + + $this->assertEquals($expected, $secci->toArray()); + } +} diff --git a/tests/Unit/Model/Request/SubModel/ContentTest.php b/tests/Unit/Model/Request/SubModel/ContentTest.php new file mode 100644 index 0000000..d436995 --- /dev/null +++ b/tests/Unit/Model/Request/SubModel/ContentTest.php @@ -0,0 +1,96 @@ +assertEquals([], $content->toArray()); + } + + public function testHandleSecci() + { + $secci = (new Secci()) + ->setDeliveryMethod('EMAIL') + ->setEmail('test@example.com'); + + $content = new Content(); + $content->setSecci($secci); + + $array = $content->toArray(); + + $expectedSecci = [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + ]; + + $this->assertEquals($expectedSecci, $array['secci']); + } + + public function testGetSecci() + { + $secci = (new Secci()) + ->setDeliveryMethod('EMAIL'); + + $content = new Content(); + $content->setSecci($secci); + + $this->assertSame($secci, $content->getSecci()); + } + + public function testSecciIsOptional() + { + $content = new Content(); + + $this->assertNull($content->getSecci()); + $this->assertArrayNotHasKey('secci', $content->toArray()); + } + + public function testThrowErrorOnWrongSecciInstance() + { + $content = new Content(); + + $this->expectException(ModelException::class); + $this->expectExceptionMessage('Model exception : Wrong class instance set to \'Secci\''); + + $content->setSecci(new Invoicing()); + } + + public function testHandleSecciByModelBuilder() + { + $builder = new ModelBuilder('Content'); + $builder->setArray([ + 'Secci' => [ + 'DeliveryMethod' => 'EMAIL', + 'Email' => 'test@example.com', + ], + ]); + + $content = $builder->getModel(); + + $this->assertInstanceOf(Secci::class, $content->getSecci()); + + $expectedSecci = [ + 'delivery-method' => ['value' => 'EMAIL'], + 'email' => ['value' => 'test@example.com'], + ]; + + $this->assertEquals($expectedSecci, $content->toArray()['secci']); + } +} diff --git a/tests/Unit/Model/Request/SubModel/HeadTest.php b/tests/Unit/Model/Request/SubModel/HeadTest.php index eafcf0d..e653490 100644 --- a/tests/Unit/Model/Request/SubModel/HeadTest.php +++ b/tests/Unit/Model/Request/SubModel/HeadTest.php @@ -38,6 +38,33 @@ public function testSubtypeSet() $this->assertTrue($head->isSubtypeSet()); } + public function testAttestationTokenIsOptional() + { + $head = new Head(); + $head->setSystemId('test-system'); + $head->setOperation('testing'); + $head->setCredential((new Credential())->setProfileId('MY_PROFILE')->setSecuritycode('5€CUr|tyK0D3')); + + $array = $head->toArray(); + + $this->assertNull($head->getAttestationToken()); + $this->assertArrayNotHasKey('attestation-token', $array); + } + + public function testHandleAttestationToken() + { + $head = new Head(); + $head->setSystemId('test-system'); + $head->setOperation('testing'); + $head->setCredential((new Credential())->setProfileId('MY_PROFILE')->setSecuritycode('5€CUr|tyK0D3')); + $head->setAttestationToken('foo-attestation-token'); + + $array = $head->toArray(); + + $this->assertEquals('foo-attestation-token', $head->getAttestationToken()); + $this->assertEquals(['value' => 'foo-attestation-token'], $array['attestation-token']); + } + public function testThrowErrorIfNoSystemId() { $head = new Head(); diff --git a/tests/Unit/Model/Response/SecciRequestTest.php b/tests/Unit/Model/Response/SecciRequestTest.php new file mode 100644 index 0000000..ff5fe99 --- /dev/null +++ b/tests/Unit/Model/Response/SecciRequestTest.php @@ -0,0 +1,194 @@ +getResponseXml($statusCode, $resultCode); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertEquals($expectedSuccess, $response->isSuccessful()); + } + + public function provideValidableResponses() + { + return [ + ['CLOSED', 200, false], + ['UNKNOWN', 504, false], + ['OK', 200, false], + ['OK', 504, true], + ]; + } + + public function testGetsSecciResult() + { + $xml = $this->getResponseXml('OK', 504); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertTrue($response->isSuccessful()); + $this->assertEquals('foo-attestation-token', $response->getAttestationToken()); + $this->assertEquals('SCE2QIAE-10092026', $response->getDocumentId()); + $this->assertNull($response->getDocument()); + $this->assertNull($response->getContentType()); + } + + public function testGetsSecciResultWithDocument() + { + $document = 'JVBERi0xLjQK + application/pdf'; + $xml = $this->getResponseXml('OK', 504, $document); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertTrue($response->isSuccessful()); + // Document is delivered base64 encoded and returned decoded + $this->assertSame("%PDF-1.4\n", $response->getDocument()); + $this->assertEquals('application/pdf', $response->getContentType()); + } + + /** @dataProvider provideEmptySecciResultFields */ + public function testIsNotSuccessfulOnEmptySecciResultField($field) + { + $xml = $this->getResponseXml('OK', 504); + $xml->content->{'secci-result'}->{$field} = ' '; + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getAttestationToken()); + $this->assertNull($response->getDocumentId()); + } + + public function provideEmptySecciResultFields() + { + return [ + ['attestation-token'], + ['document-id'], + ]; + } + + /** @dataProvider provideMissingSecciResultFields */ + public function testIsNotSuccessfulOnMissingSecciResultField($field) + { + $xml = $this->getResponseXml('OK', 504); + unset($xml->content->{'secci-result'}->{$field}); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getAttestationToken()); + $this->assertNull($response->getDocumentId()); + } + + public function provideMissingSecciResultFields() + { + return [ + ['attestation-token'], + ['document-id'], + ]; + } + + public function testIsNotSuccessfulOnMissingSecciResult() + { + $xml = $this->getResponseXml('OK', 504); + unset($xml->content->{'secci-result'}); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getAttestationToken()); + $this->assertNull($response->getDocumentId()); + } + + /** @dataProvider provideInvalidDocuments */ + public function testIsNotSuccessfulOnInvalidDocument($document) + { + $xml = $this->getResponseXml('OK', 504, $document); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getDocument()); + $this->assertNull($response->getContentType()); + } + + public function provideInvalidDocuments() + { + return [ + 'empty document' => ['application/pdf'], + 'invalid base64' => ['!!!no-base64!!!application/pdf'], + 'missing content type' => ['JVBERi0xLjQK'], + 'empty content type' => ['JVBERi0xLjQK'], + ]; + } + + public function testGettersReturnNullIfResponseIsNotSuccessful() + { + $xml = $this->getResponseXml('CLOSED', 200); + + $response = new SecciRequest($xml); + $response->validateResponse(); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getAttestationToken()); + $this->assertNull($response->getDocumentId()); + $this->assertNull($response->getDocument()); + $this->assertNull($response->getContentType()); + } + + /** + * @return \SimpleXMLElement + */ + protected function getResponseXml($statusCode, $resultCode, $document = '') + { + $content = ' + + + Example + SECCI_REQUEST + SECCI_DOCUMENT + + + 2026-09-10T11:03:58.000 + Successfully + SECCI document generated successfully + SECCI generation successful + + + + + foo-attestation-token + SCE2QIAE-10092026 + __DOCUMENT__ + + + '; + $content = str_replace('__STATUS_CODE__', $statusCode, $content); + $content = str_replace('__REASON_CODE__', '308', $content); + $content = str_replace('__RESULT_CODE__', $resultCode, $content); + $content = str_replace('__DOCUMENT__', $document, $content); + + return new \SimpleXMLElement($content); + } +} diff --git a/tests/Unit/RequestBuilderTest.php b/tests/Unit/RequestBuilderTest.php index 231da39..8c68cc9 100644 --- a/tests/Unit/RequestBuilderTest.php +++ b/tests/Unit/RequestBuilderTest.php @@ -274,4 +274,41 @@ public function testCallPaymentChangeCredit() $this->assertTrue($response->isSuccessful()); } + + public function testCallSecciRequest() + { + self::$gateway->setResponseOfPath( + '/', + $this->getSecciRequestResponse() + ); + + $builder = new RequestBuilder(true, self::$gateway->getServerRoot()); + + $head = new ModelBuilder(); + $head->setArray([ + 'SystemId' => 'SuperSystem', + 'Credential' => [ + 'ProfileId' => 'MY_AMAZING_PROFILE', + 'Securitycode' => 'super-secure-code', + ], + ]); + + $content = new ModelBuilder('Content'); + $content->setArray([ + 'Secci' => [ + 'DeliveryMethod' => 'EMAIL', + 'Email' => 'test@example.com', + ], + ]); + + $response = $builder->callSecciRequest($head, $content); + + $this->assertTrue($response->isSuccessful()); + $this->assertEquals('foo-attestation-token', $response->getAttestationToken()); + $this->assertEquals('SCE2QIAE-10092026', $response->getDocumentId()); + + $requestRaw = $builder->getRequestRaw(); + $this->assertStringContainsString('SECCI_REQUEST', $requestRaw); + $this->assertStringContainsString('EMAIL', $requestRaw); + } } diff --git a/tests/Unit/Service/ModelMapperTest.php b/tests/Unit/Service/ModelMapperTest.php index 79a7678..3a49356 100644 --- a/tests/Unit/Service/ModelMapperTest.php +++ b/tests/Unit/Service/ModelMapperTest.php @@ -33,6 +33,7 @@ public function provideRequestModels() ["PaymentChange", \RatePAY\Model\Request\PaymentChange::class], ["PaymentConfirm", \RatePAY\Model\Request\PaymentConfirm::class], ["ProfileRequest", \RatePAY\Model\Request\ProfileRequest::class], + ["SecciRequest", \RatePAY\Model\Request\SecciRequest::class], ]; } @@ -75,6 +76,7 @@ public function provideRequestSubModels() ['Payment', \RatePAY\Model\Request\SubModel\Content\Payment::class], ['InstallmentDetails', \RatePAY\Model\Request\SubModel\Content\Payment\InstallmentDetails::class], ['Invoicing', \RatePAY\Model\Request\SubModel\Content\Invoicing::class], + ['Secci', \RatePAY\Model\Request\SubModel\Content\Secci::class], ]; } @@ -98,6 +100,14 @@ public function provideResponseModels() ['PaymentQuery', \RatePAY\Model\Response\PaymentQuery::class], ['PaymentRequest', \RatePAY\Model\Response\PaymentRequest::class], ['ProfileRequest', \RatePAY\Model\Response\ProfileRequest::class], + ['SecciRequest', \RatePAY\Model\Response\SecciRequest::class], ]; } + + public function testGetRequestModels() + { + $requestModels = ModelMapper::getRequestModels(); + + $this->assertContains('SecciRequest', $requestModels); + } }