diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5cda1d4..80358b6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.3.0] - 2024-07-18
+
+### Added
+
+- Support for `attempt_failed` payment status
+
## [2.2.0] - 2024-07-15
### Added
diff --git a/README.md b/README.md
index 1a40352b..bf4486fe 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,9 @@
4. [Executed Status](#status-executed)
5. [Settled Status](#status-settled)
6. [Failed Status](#status-failed)
- 7. [Authorization flow config](#auth-flow-config)
- 8. [Source of funds](#source-of-funds)
+ 7. [Attempt Failed Status](#status-attempt-failed)
+ 8. [Authorization flow config](#auth-flow-config)
+ 9. [Source of funds](#source-of-funds)
7. [Authorizing a payment](#authorizing-payment)
8. [Refunds](#refunds)
9. [Payouts](#payouts)
@@ -418,7 +419,8 @@ $payment->isAuthorizing();
$payment->isAuthorized(); // Will also return false when the payment has progressed to executed, failed or settled states.
$payment->isExecuted(); // Will also return false when the payment has progressed to failed or settled states.
$payment->isSettled();
-$payment->isFailed();
+$payment->isFailed(); // Payment has failed
+$payment->isAttemptFailed(); // Payment attempt has failed, only available if payment retries are enabled.
```
Or you can get the status as a string and compare it to the provided constants in `PaymentStatus`:
@@ -587,6 +589,23 @@ if ($payment instanceof PaymentFailedInterface) {
}
```
+
+
+### Attempt Failed Status
+
+> Status only available when you enable payment retries.
+
+```php
+use TrueLayer\Interfaces\Payment\PaymentAttemptFailedInterface;
+
+if ($payment instanceof PaymentAttemptFailedInterface) {
+ $payment->getFailedAt(); // The date and time the payment failed at
+ $payment->getFailureStage(); // The status the payment was when it failed, one of `authorization_required`, `authorizing` or `authorized`
+ $payment->getFailureReason(); // The reason the payment failed. Handle unexpected values gracefully as an unknown failure.
+ $payment->getAuthorizationFlowConfig(); // see authorization flow config
+}
+```
+
### Authorization flow config
diff --git a/config/bindings.php b/config/bindings.php
index 3aa2f15a..0f52051b 100644
--- a/config/bindings.php
+++ b/config/bindings.php
@@ -23,11 +23,13 @@
Interfaces\Payment\PaymentExecutedInterface::class => Entities\Payment\PaymentRetrieved\PaymentExecuted::class,
Interfaces\Payment\PaymentSettledInterface::class => Entities\Payment\PaymentRetrieved\PaymentSettled::class,
Interfaces\Payment\PaymentFailedInterface::class => Entities\Payment\PaymentRetrieved\PaymentFailed::class,
+ Interfaces\Payment\PaymentAttemptFailedInterface::class => Entities\Payment\PaymentRetrieved\PaymentAttemptFailed::class,
Interfaces\Payment\PaymentSourceInterface::class => Entities\Payment\PaymentRetrieved\PaymentSource::class,
TrueLayer\Interfaces\Payment\StartAuthorizationFlowRequestInterface::class => Entities\Payment\AuthorizationFlow\StartAuthorizationFlowRequest::class,
Interfaces\Payment\AuthorizationFlow\AuthorizationFlowInterface::class => Entities\Payment\AuthorizationFlow\AuthorizationFlow::class,
Interfaces\Payment\AuthorizationFlow\ConfigurationInterface::class => Entities\Payment\AuthorizationFlow\Configuration::class,
+ Interfaces\Payment\AuthorizationFlow\ActionInterface::class => Entities\Payment\AuthorizationFlow\Action::class,
Interfaces\Payment\AuthorizationFlow\Action\ProviderSelectionActionInterface::class => Entities\Payment\AuthorizationFlow\Action\ProviderSelectionAction::class,
Interfaces\Payment\AuthorizationFlow\Action\RedirectActionInterface::class => Entities\Payment\AuthorizationFlow\Action\RedirectAction::class,
Interfaces\Payment\AuthorizationFlow\Action\WaitActionInterface::class => Entities\Payment\AuthorizationFlow\Action\WaitAction::class,
diff --git a/config/discriminations.php b/config/discriminations.php
index 21b6472f..8343e278 100644
--- a/config/discriminations.php
+++ b/config/discriminations.php
@@ -23,6 +23,7 @@
PaymentStatus::EXECUTED => Interfaces\Payment\PaymentExecutedInterface::class,
PaymentStatus::SETTLED => Interfaces\Payment\PaymentSettledInterface::class,
PaymentStatus::FAILED => Interfaces\Payment\PaymentFailedInterface::class,
+ PaymentStatus::ATTEMPT_FAILED => Interfaces\Payment\PaymentAttemptFailedInterface::class,
],
Interfaces\Payment\AuthorizationFlow\AuthorizationFlowResponseInterface::class => [
'discriminate_on' => 'status',
diff --git a/src/Constants/AuthorizationFlowActionTypes.php b/src/Constants/AuthorizationFlowActionTypes.php
index 2b417ad3..a2b68a6c 100644
--- a/src/Constants/AuthorizationFlowActionTypes.php
+++ b/src/Constants/AuthorizationFlowActionTypes.php
@@ -9,4 +9,5 @@ class AuthorizationFlowActionTypes
public const PROVIDER_SELECTION = 'provider_selection';
public const REDIRECT = 'redirect';
public const WAIT = 'wait';
+ public const RETRY = 'retry';
}
diff --git a/src/Constants/PaymentStatus.php b/src/Constants/PaymentStatus.php
index 5e3726db..a42f5045 100644
--- a/src/Constants/PaymentStatus.php
+++ b/src/Constants/PaymentStatus.php
@@ -11,5 +11,6 @@ class PaymentStatus
public const AUTHORIZED = 'authorized';
public const EXECUTED = 'executed';
public const FAILED = 'failed';
+ public const ATTEMPT_FAILED = 'attempt_failed';
public const SETTLED = 'settled';
}
diff --git a/src/Entities/Payment/AuthorizationFlow/Action.php b/src/Entities/Payment/AuthorizationFlow/Action.php
index 21068665..96434a07 100644
--- a/src/Entities/Payment/AuthorizationFlow/Action.php
+++ b/src/Entities/Payment/AuthorizationFlow/Action.php
@@ -5,12 +5,27 @@
namespace TrueLayer\Entities\Payment\AuthorizationFlow;
use TrueLayer\Entities\Entity;
-use TrueLayer\Interfaces\Payment\AuthorizationFlow\ActionInterface;
+use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\ActionInterface;
-abstract class Action extends Entity implements ActionInterface
+class Action extends Entity implements ActionInterface
{
+ /**
+ * @var string
+ */
+ protected string $type;
+
+ /**
+ * @var string[]
+ */
+ protected array $arrayFields = [
+ 'type',
+ ];
+
/**
* @return string
*/
- abstract public function getType(): string;
+ public function getType(): string
+ {
+ return $this->type;
+ }
}
diff --git a/src/Entities/Payment/AuthorizationFlow/Action/ProviderSelectionAction.php b/src/Entities/Payment/AuthorizationFlow/Action/ProviderSelectionAction.php
index 5c875d2e..1c9312c7 100644
--- a/src/Entities/Payment/AuthorizationFlow/Action/ProviderSelectionAction.php
+++ b/src/Entities/Payment/AuthorizationFlow/Action/ProviderSelectionAction.php
@@ -4,7 +4,6 @@
namespace TrueLayer\Entities\Payment\AuthorizationFlow\Action;
-use TrueLayer\Constants\AuthorizationFlowActionTypes;
use TrueLayer\Entities\Payment\AuthorizationFlow\Action;
use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\ProviderSelectionActionInterface;
use TrueLayer\Interfaces\Provider\ProviderInterface;
@@ -38,12 +37,4 @@ public function getProviders(): array
{
return $this->providers;
}
-
- /**
- * @return string
- */
- public function getType(): string
- {
- return AuthorizationFlowActionTypes::PROVIDER_SELECTION;
- }
}
diff --git a/src/Entities/Payment/AuthorizationFlow/Action/RedirectAction.php b/src/Entities/Payment/AuthorizationFlow/Action/RedirectAction.php
index d71e57fd..d360271c 100644
--- a/src/Entities/Payment/AuthorizationFlow/Action/RedirectAction.php
+++ b/src/Entities/Payment/AuthorizationFlow/Action/RedirectAction.php
@@ -4,7 +4,6 @@
namespace TrueLayer\Entities\Payment\AuthorizationFlow\Action;
-use TrueLayer\Constants\AuthorizationFlowActionTypes;
use TrueLayer\Entities\Payment\AuthorizationFlow\Action;
use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\RedirectActionInterface;
use TrueLayer\Interfaces\Provider\ProviderInterface;
@@ -45,14 +44,6 @@ public function getUri(): string
return $this->uri;
}
- /**
- * @return string
- */
- public function getType(): string
- {
- return AuthorizationFlowActionTypes::REDIRECT;
- }
-
/**
* @return string
*/
diff --git a/src/Entities/Payment/AuthorizationFlow/Action/WaitAction.php b/src/Entities/Payment/AuthorizationFlow/Action/WaitAction.php
index 6483ecc4..ab0b2236 100644
--- a/src/Entities/Payment/AuthorizationFlow/Action/WaitAction.php
+++ b/src/Entities/Payment/AuthorizationFlow/Action/WaitAction.php
@@ -4,24 +4,10 @@
namespace TrueLayer\Entities\Payment\AuthorizationFlow\Action;
-use TrueLayer\Constants\AuthorizationFlowActionTypes;
use TrueLayer\Entities\Payment\AuthorizationFlow\Action;
use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\WaitActionInterface;
class WaitAction extends Action implements WaitActionInterface
{
- /**
- * @var string[]
- */
- protected array $arrayFields = [
- 'type',
- ];
- /**
- * @return string
- */
- public function getType(): string
- {
- return AuthorizationFlowActionTypes::WAIT;
- }
}
diff --git a/src/Entities/Payment/PaymentRetrieved.php b/src/Entities/Payment/PaymentRetrieved.php
index 6784463c..b7bc2a13 100644
--- a/src/Entities/Payment/PaymentRetrieved.php
+++ b/src/Entities/Payment/PaymentRetrieved.php
@@ -177,6 +177,14 @@ public function isFailed(): bool
return $this->getStatus() === PaymentStatus::FAILED;
}
+ /**
+ * @return bool
+ */
+ public function isAttemptFailed(): bool
+ {
+ return $this->getStatus() === PaymentStatus::ATTEMPT_FAILED;
+ }
+
/**
* @return bool
*/
diff --git a/src/Entities/Payment/PaymentRetrieved/PaymentAttemptFailed.php b/src/Entities/Payment/PaymentRetrieved/PaymentAttemptFailed.php
new file mode 100644
index 00000000..418a281c
--- /dev/null
+++ b/src/Entities/Payment/PaymentRetrieved/PaymentAttemptFailed.php
@@ -0,0 +1,11 @@
+ \DateTimeInterface::class,
- ]);
- }
-
- /**
- * @return mixed[]
- */
- protected function arrayFields(): array
- {
- return \array_merge(parent::arrayFields(), [
- 'failed_at',
- 'failure_stage',
- 'failure_reason',
- ]);
- }
-
- /**
- * @return \DateTimeInterface
- */
- public function getFailedAt(): \DateTimeInterface
- {
- return $this->failedAt;
- }
-
- /**
- * @return string
- */
- public function getFailureStage(): string
- {
- return $this->failureStage;
- }
-
- /**
- * @return string|null
- */
- public function getFailureReason(): ?string
- {
- return $this->failureReason ?? null;
- }
}
diff --git a/src/Entities/Payment/PaymentRetrieved/PaymentFailure.php b/src/Entities/Payment/PaymentRetrieved/PaymentFailure.php
new file mode 100644
index 00000000..f7731a09
--- /dev/null
+++ b/src/Entities/Payment/PaymentRetrieved/PaymentFailure.php
@@ -0,0 +1,69 @@
+ \DateTimeInterface::class,
+ ]);
+ }
+
+ /**
+ * @return mixed[]
+ */
+ protected function arrayFields(): array
+ {
+ return \array_merge(parent::arrayFields(), [
+ 'failed_at',
+ 'failure_stage',
+ 'failure_reason',
+ ]);
+ }
+
+ /**
+ * @return \DateTimeInterface
+ */
+ public function getFailedAt(): \DateTimeInterface
+ {
+ return $this->failedAt;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFailureStage(): string
+ {
+ return $this->failureStage;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getFailureReason(): ?string
+ {
+ return $this->failureReason ?? null;
+ }
+}
diff --git a/src/Interfaces/Payment/AuthorizationFlow/Action/ActionInterface.php b/src/Interfaces/Payment/AuthorizationFlow/Action/ActionInterface.php
index 89b07010..b20afa3f 100644
--- a/src/Interfaces/Payment/AuthorizationFlow/Action/ActionInterface.php
+++ b/src/Interfaces/Payment/AuthorizationFlow/Action/ActionInterface.php
@@ -4,12 +4,7 @@
namespace TrueLayer\Interfaces\Payment\AuthorizationFlow\Action;
-use TrueLayer\Interfaces\ArrayableInterface;
-
-interface ActionInterface extends ArrayableInterface
+interface ActionInterface extends \TrueLayer\Interfaces\Payment\AuthorizationFlow\ActionInterface
{
- /**
- * @return string
- */
- public function getType(): string;
+
}
diff --git a/src/Interfaces/Payment/AuthorizationFlow/ActionInterface.php b/src/Interfaces/Payment/AuthorizationFlow/ActionInterface.php
index d837947d..82ce88a6 100644
--- a/src/Interfaces/Payment/AuthorizationFlow/ActionInterface.php
+++ b/src/Interfaces/Payment/AuthorizationFlow/ActionInterface.php
@@ -8,4 +8,8 @@
interface ActionInterface extends ArrayableInterface
{
+ /**
+ * @return string
+ */
+ public function getType(): string;
}
diff --git a/src/Interfaces/Payment/PaymentAttemptFailedInterface.php b/src/Interfaces/Payment/PaymentAttemptFailedInterface.php
new file mode 100644
index 00000000..ff252f11
--- /dev/null
+++ b/src/Interfaces/Payment/PaymentAttemptFailedInterface.php
@@ -0,0 +1,9 @@
+client()->getMerchantAccounts(),
- fn(MerchantAccountInterface $account) => $account->getCurrency() === 'GBP'
- );
-
- $merchantBeneficiary = $helper->merchantBeneficiary($account);
-
- $created = $helper->create(
- $helper->bankTransferMethod($merchantBeneficiary)->enablePaymentRetry(), $helper->user(), $account->getCurrency()
- );
-
- \expect($created)->toBeInstanceOf(PaymentCreatedInterface::class);
-
- /** @var BankTransferPaymentMethodInterface $paymentMethod */
- $paymentMethod = $created->getDetails()->getPaymentMethod();
- \expect($paymentMethod->isPaymentRetryEnabled())->toBe(true);
-});
-
\it('starts payment authorization', function (PaymentCreatedInterface $created) {
$response = \client()->startPaymentAuthorization($created, 'https://console.truelayer.com/redirect-page');
@@ -267,3 +248,46 @@
return $created;
});
+
+\it('handles a merchant payment with retry enabled', function () {
+ $helper = \paymentHelper();
+ $client = $helper->client();
+
+ $account = Arr::first(
+ $helper->client()->getMerchantAccounts(),
+ fn(MerchantAccountInterface $account) => $account->getCurrency() === 'GBP'
+ );
+
+ $merchantBeneficiary = $helper->merchantBeneficiary($account);
+ $paymentMethod = $helper->bankTransferMethod($merchantBeneficiary)->enablePaymentRetry();
+ $created = $helper->create($paymentMethod, $helper->user(), $account->getCurrency());
+
+ \expect($created)->toBeInstanceOf(PaymentCreatedInterface::class);
+
+ /** @var BankTransferPaymentMethodInterface $paymentMethod */
+ $paymentMethod = $created->getDetails()->getPaymentMethod();
+ \expect($paymentMethod->isPaymentRetryEnabled())->toBe(true);
+
+ $payload = $client->paymentAuthorizationFlow($created)
+ ->returnUri('https://console.truelayer.com/redirect-page')
+ ->enableProviderSelection()
+ ->toArray();
+ $payload['retry'] = new stdClass();
+
+ $client->getApiClient()->request()
+ ->payload($payload)
+ ->uri(\str_replace('{id}', $created->getId(), Endpoints::PAYMENTS_START_AUTH_FLOW))
+ ->post();
+
+ $client->submitPaymentProvider($created, 'mock-payments-gb-redirect');
+
+ /** @var RedirectActionInterface $next */
+ $next = $created->getDetails()->getAuthorizationFlowNextAction();
+ \bankAction($next->getUri(), 'RejectExecution');
+ \sleep(20);
+
+ /** @var PaymentAttemptFailedInterface $retrievedPayment */
+ $retrievedPayment = $created->getDetails();
+ expect($retrievedPayment)->toBeInstanceOf(PaymentAttemptFailedInterface::class);
+ expect($retrievedPayment->getPaymentMethod()->isPaymentRetryEnabled())->toBe(true);
+});
diff --git a/tests/integration/Mocks/PaymentResponse.php b/tests/integration/Mocks/PaymentResponse.php
index 7affc47a..9e31c2cc 100644
--- a/tests/integration/Mocks/PaymentResponse.php
+++ b/tests/integration/Mocks/PaymentResponse.php
@@ -94,4 +94,8 @@ public static function failed(): Response
return new Response(200, [], '{"id":"401cfaa1-8d44-4306-a2f9-a0a6e365f570","amount_in_minor":1,"metadata":{"metadata_key_1":"metadata_value_1","metadata_key_2":"metadata_value_2","metadata_key_3":"metadata_value_3"},"currency":"GBP","user":{"id":"7ed73602-c8bc-4b2f-8a96-9490a6ea5983"},"payment_method":{"type":"bank_transfer","beneficiary":{"type":"external_account","account_identifier":{"type":"sort_code_account_number","sort_code":"010203","account_number":"12345678"},"account_holder_name":"Bob","reference":"TEST"},"provider_selection":{"type":"user_selected"}},"created_at":"2022-02-06T22:25:43.899669Z","status":"failed","authorization_flow":{"configuration":{"provider_selection":{},"redirect":{"return_uri":"https://penny.t7r.dev/redirect/v3"}}},"failed_at":"2022-02-06T22:26:48.849469Z","failure_stage":"authorizing","failure_reason":"authorization_failed"}');
}
+ public static function attemptFailed(): Response
+ {
+ return new Response(200, [], '{"id":"401cfaa1-8d44-4306-a2f9-a0a6e365f570","amount_in_minor":1,"metadata":{"metadata_key_1":"metadata_value_1","metadata_key_2":"metadata_value_2","metadata_key_3":"metadata_value_3"},"currency":"GBP","user":{"id":"7ed73602-c8bc-4b2f-8a96-9490a6ea5983"},"payment_method":{"retry": {},"type":"bank_transfer","beneficiary":{"type":"external_account","account_identifier":{"type":"sort_code_account_number","sort_code":"010203","account_number":"12345678"},"account_holder_name":"Bob","reference":"TEST"},"provider_selection":{"type":"user_selected"}},"created_at":"2022-02-06T22:25:43.899669Z","status":"attempt_failed","authorization_flow":{"configuration":{"retry": {},"provider_selection":{},"redirect":{"return_uri":"https://penny.t7r.dev/redirect/v3"}}},"failed_at":"2022-02-06T22:26:48.849469Z","failure_stage":"authorizing","failure_reason":"authorization_failed"}');
+ }
}
diff --git a/tests/integration/PaymentRetrievalTest.php b/tests/integration/PaymentRetrievalTest.php
index 874f3504..0fc89ef4 100644
--- a/tests/integration/PaymentRetrievalTest.php
+++ b/tests/integration/PaymentRetrievalTest.php
@@ -13,6 +13,7 @@
use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\RedirectActionInterface;
use TrueLayer\Interfaces\Payment\AuthorizationFlow\Action\WaitActionInterface;
use TrueLayer\Interfaces\Payment\AuthorizationFlow\ConfigurationInterface;
+use TrueLayer\Interfaces\Payment\PaymentAttemptFailedInterface;
use TrueLayer\Interfaces\Payment\PaymentAuthorizationRequiredInterface;
use TrueLayer\Interfaces\Payment\PaymentAuthorizedInterface;
use TrueLayer\Interfaces\Payment\PaymentAuthorizingInterface;
@@ -254,6 +255,7 @@ function assertPaymentCommon(PaymentRetrievedInterface $payment)
\expect($payment->isExecuted())->toBe(false);
\expect($payment->isSettled())->toBe(false);
\expect($payment->isFailed())->toBe(true);
+ \expect($payment->isAttemptFailed())->toBe(false);
\expect($payment->getFailedAt()->format(DateTime::FORMAT))->toBe('2022-02-06T22:26:48.849469Z');
\expect($payment->getFailureStage())->toBe('authorizing');
\expect($payment->getFailureReason())->toBe('authorization_failed');
@@ -266,6 +268,26 @@ function assertPaymentCommon(PaymentRetrievedInterface $payment)
\assertPaymentCommon($payment);
});
+\it('handles payment attempt failed', function () {
+ /** @var PaymentAttemptFailedInterface $payment */
+ $payment = \client(PaymentResponse::attemptFailed())->getPayment('1');
+
+ \expect($payment)->toBeInstanceOf(PaymentAttemptFailedInterface::class);
+ \expect($payment->getAuthorizationFlowConfig())->toBeInstanceOf(ConfigurationInterface::class);
+ \expect($payment->isAuthorizationRequired())->toBe(false);
+ \expect($payment->isAuthorizing())->toBe(false);
+ \expect($payment->isAuthorized())->toBe(false);
+ \expect($payment->isExecuted())->toBe(false);
+ \expect($payment->isSettled())->toBe(false);
+ \expect($payment->isFailed())->toBe(false);
+ \expect($payment->isAttemptFailed())->toBe(true);
+ \expect($payment->getFailedAt()->format(DateTime::FORMAT))->toBe('2022-02-06T22:26:48.849469Z');
+ \expect($payment->getFailureStage())->toBe('authorizing');
+ \expect($payment->getFailureReason())->toBe('authorization_failed');
+
+ \assertPaymentCommon($payment);
+});
+
\it('handles payment with no auth flow config', function () {
/** @var PaymentExecutedInterface $payment */
$payment = \client(PaymentResponse::executedNoAuthFlow())->getPayment('1');