diff --git a/docs/core/reference/payments.md b/docs/core/reference/payments.md
index c4e0479dca..ce56261e1a 100644
--- a/docs/core/reference/payments.md
+++ b/docs/core/reference/payments.md
@@ -330,6 +330,20 @@ $order->charges; // Get all transactions that are charges.
$order->refunds; // Get all transactions that are refunds.
```
+### Payment Checks
+
+Some providers return information based on checks that can occur before payment is validated and completed.
+This is usually related to 3DSecure but in some cases can relate to credit checks or anything the payment provider has deemed relevant to the transaction.
+
+You can get access to these checks via the `paymentChecks()` method on the `Transaction`.
+
+```php
+foreach($transaction->paymentChecks() as $check) {
+ $check->successful;
+ $check->label;
+ $check->message;
+}
+```
## Payments
We will be looking to add support for the most popular payment providers, so keep an eye out here as we will list them all out.
diff --git a/packages/admin/resources/views/infolists/components/transaction.blade.php b/packages/admin/resources/views/infolists/components/transaction.blade.php
index 51cdc4a977..ef0ada5568 100644
--- a/packages/admin/resources/views/infolists/components/transaction.blade.php
+++ b/packages/admin/resources/views/infolists/components/transaction.blade.php
@@ -19,7 +19,7 @@
'!border-red-500 bg-red-50' => !$transaction->success,
'bg-gray-50' => $transaction->success,
])
->
+>
{{ $transaction->driver }} //
@@ -53,7 +53,7 @@
@endif
-
!$transaction->success,
@@ -75,26 +75,19 @@
class="w-4"
/>
-
{{ $transaction->created_at->format('jS F Y h:ia') }}
- @if($threeD = $transaction->meta['threedSecure'] ?? null)
-
- @foreach([
- 'address' => 'Address',
- 'postalCode' => 'Postal Code',
- 'securityCode' => 'Security Code',
- ] as $metaKey => $metaLabel)
-
- {{ $metaLabel }}
-
- @endforeach
-
- @endif
+
+ @foreach($transaction->paymentChecks() as $check)
+
+ {{ $check->label }}: {{ $check->message }}
+
+ @endforeach
+
@if($transaction->notes)
@@ -111,7 +104,7 @@ class="w-4"
@endif
- !$transaction->success,
diff --git a/packages/core/src/Base/DataTransferObjects/PaymentCheck.php b/packages/core/src/Base/DataTransferObjects/PaymentCheck.php
new file mode 100644
index 0000000000..a64f196841
--- /dev/null
+++ b/packages/core/src/Base/DataTransferObjects/PaymentCheck.php
@@ -0,0 +1,13 @@
+checks[] = $check;
+ }
+
+ public function getChecks(): array
+ {
+ return $this->checks;
+ }
+
+ public function current(): PaymentCheck
+ {
+ return $this->checks[$this->position];
+ }
+
+ public function next(): void
+ {
+ $this->position++;
+ }
+
+ public function key(): mixed
+ {
+ return $this->position;
+ }
+
+ public function valid(): bool
+ {
+ return isset($this->checks[$this->position]);
+ }
+
+ public function rewind(): void
+ {
+ $this->position = 0;
+ }
+
+ public function offsetExists(mixed $offset): bool
+ {
+ return isset($this->checks[$offset]);
+ }
+
+ public function offsetGet(mixed $offset): mixed
+ {
+ return $this->checks[$offset];
+ }
+
+ public function offsetSet(mixed $offset, mixed $value): void
+ {
+ if (is_null($offset)) {
+ $this->checks[] = $value;
+ } else {
+ $this->checks[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset(mixed $offset): void
+ {
+ unset($this->checks[$offset]);
+ }
+}
diff --git a/packages/core/src/Models/Transaction.php b/packages/core/src/Models/Transaction.php
index 0386c5396e..cb672a18da 100644
--- a/packages/core/src/Models/Transaction.php
+++ b/packages/core/src/Models/Transaction.php
@@ -87,6 +87,11 @@ public function driver()
return Payments::driver($this->driver);
}
+ public function paymentChecks()
+ {
+ return $this->driver()->getPaymentChecks($this);
+ }
+
public function refund(int $amount, $notes = null)
{
return $this->driver()->refund($this, $amount, $notes);
diff --git a/packages/core/src/PaymentTypes/AbstractPayment.php b/packages/core/src/PaymentTypes/AbstractPayment.php
index 27d1c4f9e7..f3cfc7d59a 100644
--- a/packages/core/src/PaymentTypes/AbstractPayment.php
+++ b/packages/core/src/PaymentTypes/AbstractPayment.php
@@ -2,9 +2,11 @@
namespace Lunar\PaymentTypes;
+use Lunar\Base\DataTransferObjects\PaymentChecks;
use Lunar\Base\PaymentTypeInterface;
use Lunar\Models\Cart;
use Lunar\Models\Order;
+use Lunar\Models\Transaction;
abstract class AbstractPayment implements PaymentTypeInterface
{
@@ -71,4 +73,9 @@ public function setConfig(array $config): self
return $this;
}
+
+ public function getPaymentChecks(Transaction $transaction): PaymentChecks
+ {
+ return new PaymentChecks;
+ }
}
diff --git a/packages/opayo/src/OpayoPaymentType.php b/packages/opayo/src/OpayoPaymentType.php
index a1509f380b..ad300a7b3f 100644
--- a/packages/opayo/src/OpayoPaymentType.php
+++ b/packages/opayo/src/OpayoPaymentType.php
@@ -4,6 +4,8 @@
use Illuminate\Support\Str;
use Lunar\Base\DataTransferObjects\PaymentCapture;
+use Lunar\Base\DataTransferObjects\PaymentCheck;
+use Lunar\Base\DataTransferObjects\PaymentChecks;
use Lunar\Base\DataTransferObjects\PaymentRefund;
use Lunar\Events\PaymentAttemptEvent;
use Lunar\Models\Order;
@@ -421,6 +423,82 @@ public function setPolicy(mixed $policy): void
$this->policy = $policy;
}
+ public function getPaymentChecks(Transaction $transaction): PaymentChecks
+ {
+ $meta = $transaction->meta['threedSecure'] ?? null;
+
+ $checks = new PaymentChecks;
+
+ if (! $meta) {
+ return $checks;
+ }
+
+ if (isset($meta['address'])) {
+ $message = $meta['address'];
+ $successful = $meta['address'] == 'Matched';
+
+ if (is_bool($message) && ! $message) {
+ $message = 'NotMatched';
+ $successful = false;
+ }
+ if (is_bool($message) && $message) {
+ $message = 'Matched';
+ $successful = true;
+ }
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $successful,
+ label: 'Address',
+ message: $message,
+ )
+ );
+ }
+
+ if (isset($meta['postalCode'])) {
+ $message = $meta['postalCode'];
+ $successful = $meta['postalCode'] == 'Matched';
+
+ if (is_bool($message) && ! $message) {
+ $message = 'NotMatched';
+ $successful = false;
+ }
+ if (is_bool($message) && $message) {
+ $message = 'Matched';
+ $successful = true;
+ }
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $successful,
+ label: 'Postal Code',
+ message: $message,
+ )
+ );
+ }
+
+ if (isset($meta['securityCode'])) {
+ $message = $meta['securityCode'];
+ $successful = $meta['securityCode'] == 'Matched';
+
+ if (is_bool($message) && ! $message) {
+ $message = 'NotMatched';
+ $successful = false;
+ }
+ if (is_bool($message) && $message) {
+ $message = 'Matched';
+ $successful = true;
+ }
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $successful,
+ label: 'Security Code',
+ message: $message,
+ )
+ );
+ }
+
+ return $checks;
+ }
+
private function saveCard(Order $order, object $details, string $authCode = null)
{
if (! $order->user_id) {
diff --git a/packages/stripe/src/StripePaymentType.php b/packages/stripe/src/StripePaymentType.php
index fa8608b342..c697b23c3b 100644
--- a/packages/stripe/src/StripePaymentType.php
+++ b/packages/stripe/src/StripePaymentType.php
@@ -4,6 +4,8 @@
use Lunar\Base\DataTransferObjects\PaymentAuthorize;
use Lunar\Base\DataTransferObjects\PaymentCapture;
+use Lunar\Base\DataTransferObjects\PaymentCheck;
+use Lunar\Base\DataTransferObjects\PaymentChecks;
use Lunar\Base\DataTransferObjects\PaymentRefund;
use Lunar\Events\PaymentAttemptEvent;
use Lunar\Exceptions\DisallowMultipleCartOrdersException;
@@ -193,4 +195,43 @@ public function refund(Transaction $transaction, int $amount = 0, $notes = null)
success: true
);
}
+
+ public function getPaymentChecks(Transaction $transaction): PaymentChecks
+ {
+ $meta = $transaction->meta;
+
+ $checks = new PaymentChecks;
+
+ if (isset($meta['address_line1_check'])) {
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $meta['address_line1_check'] == 'pass',
+ label: 'Address Line 1',
+ message: $meta['address_line1_check'],
+ )
+ );
+ }
+
+ if (isset($meta['address_postal_code_check'])) {
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $meta['address_postal_code_check'] == 'pass',
+ label: 'Postal Code',
+ message: $meta['address_postal_code_check'],
+ )
+ );
+ }
+
+ if (isset($meta['cvc_check'])) {
+ $checks->addCheck(
+ new PaymentCheck(
+ successful: $meta['cvc_check'] == 'pass',
+ label: 'CVC Check',
+ message: $meta['cvc_check'],
+ )
+ );
+ }
+
+ return $checks;
+ }
}
diff --git a/tests/opayo/Feature/OpayoPaymentTypeTest.php b/tests/opayo/Feature/OpayoPaymentTypeTest.php
index 75f280db76..c4049b3f39 100644
--- a/tests/opayo/Feature/OpayoPaymentTypeTest.php
+++ b/tests/opayo/Feature/OpayoPaymentTypeTest.php
@@ -134,3 +134,105 @@
'last_four' => '1111',
]);
});
+
+it('can return correct payment checks', function () {
+ \Lunar\Models\Currency::factory()->create();
+
+ $cart = buildCart();
+
+ $order = $cart->createOrder();
+
+ $transactionA = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'opayo',
+ 'meta' => [
+ 'threedSecure' => [
+ 'address' => 'Matched',
+ 'postalCode' => 'Matched',
+ 'securityCode' => 'Matched',
+ ],
+ ],
+ ]);
+
+ $transactionB = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'opayo',
+ 'meta' => [
+ 'threedSecure' => [
+ 'address' => true,
+ 'postalCode' => true,
+ 'securityCode' => true,
+ ],
+ ],
+ ]);
+
+ $transactionC = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'opayo',
+ 'meta' => [
+ 'threedSecure' => [
+ 'address' => 'NotMatched',
+ 'postalCode' => 'NotMatched',
+ 'securityCode' => 'NotMatched',
+ ],
+ ],
+ ]);
+
+ $transactionD = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'opayo',
+ 'meta' => [
+ 'threedSecure' => [
+ 'address' => false,
+ 'postalCode' => false,
+ 'securityCode' => false,
+ ],
+ ],
+ ]);
+
+ $paymentAChecks = $transactionA->paymentChecks();
+
+ expect($paymentAChecks)->toHaveCount(3)
+ ->and($paymentAChecks[0]->successful)
+ ->toBe(true)
+ ->and($paymentAChecks[1]->successful)
+ ->toBe(true)
+ ->and($paymentAChecks[2]->successful)
+ ->toBe(true);
+
+ $paymentBChecks = $transactionB->paymentChecks();
+
+ expect($paymentBChecks)->toHaveCount(3)
+ ->and($paymentBChecks[0]->successful)
+ ->toBe(true)
+ ->and($paymentBChecks[1]->successful)
+ ->toBe(true)
+ ->and($paymentBChecks[2]->successful)
+ ->toBe(true);
+
+ $paymentCChecks = $transactionC->paymentChecks();
+
+ expect($paymentCChecks)->toHaveCount(3)
+ ->and($paymentCChecks[0]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentCChecks[1]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentCChecks[2]->successful)
+ ->not
+ ->toBe(true);
+
+ $paymentDChecks = $transactionD->paymentChecks();
+
+ expect($paymentDChecks)->toHaveCount(3)
+ ->and($paymentCChecks[0]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentDChecks[1]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentDChecks[2]->successful)
+ ->not
+ ->toBe(true);
+});
diff --git a/tests/stripe/Unit/StripePaymentTypeTest.php b/tests/stripe/Unit/StripePaymentTypeTest.php
index e9ff0cbc84..0ad8f4e9f6 100644
--- a/tests/stripe/Unit/StripePaymentTypeTest.php
+++ b/tests/stripe/Unit/StripePaymentTypeTest.php
@@ -50,7 +50,7 @@
'type' => 'capture',
'success' => false,
]);
-})->group('foo');
+});
it('can retrieve existing payment intent', function () {
$cart = CartBuilder::build([
@@ -111,3 +111,101 @@
expect($cart->refresh()->completedOrder)->toBeNull();
});
+
+it('can return correct payment checks', function () {
+ \Lunar\Models\Currency::factory()->create();
+
+ $cart = buildCart();
+
+ $order = $cart->createOrder();
+
+ $transactionA = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'stripe',
+ 'meta' => [
+ 'address_line1_check' => 'pass',
+ 'address_postal_code_check' => 'pass',
+ 'cvc_check' => 'pass',
+ ],
+ ]);
+
+ $transactionB = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'stripe',
+ 'meta' => [
+ 'address_line1_check' => 'fail',
+ 'address_postal_code_check' => 'fail',
+ 'cvc_check' => 'fail',
+ ],
+ ]);
+
+ $transactionC = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'stripe',
+ 'meta' => [
+ 'address_line1_check' => 'unavailable',
+ 'address_postal_code_check' => 'unavailable',
+ 'cvc_check' => 'unavailable',
+ ],
+ ]);
+
+ $transactionD = \Lunar\Models\Transaction::factory()->create([
+ 'order_id' => $order->id,
+ 'driver' => 'stripe',
+ 'meta' => [
+ 'address_line1_check' => 'unchecked',
+ 'address_postal_code_check' => 'unchecked',
+ 'cvc_check' => 'unchecked',
+ ],
+ ]);
+
+ $paymentAChecks = $transactionA->paymentChecks();
+
+ expect($paymentAChecks)->toHaveCount(3)
+ ->and($paymentAChecks[0]->successful)
+ ->toBe(true)
+ ->and($paymentAChecks[1]->successful)
+ ->toBe(true)
+ ->and($paymentAChecks[2]->successful)
+ ->toBe(true);
+
+ $paymentBChecks = $transactionB->paymentChecks();
+
+ expect($paymentBChecks)->toHaveCount(3)
+ ->and($paymentBChecks[0]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentBChecks[1]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentBChecks[2]->successful)
+ ->not
+ ->toBe(true);
+
+ $paymentCChecks = $transactionC->paymentChecks();
+
+ expect($paymentCChecks)->toHaveCount(3)
+ ->and($paymentCChecks[0]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentCChecks[1]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentCChecks[2]->successful)
+ ->not
+ ->toBe(true);
+
+ $paymentDChecks = $transactionD->paymentChecks();
+
+ expect($paymentDChecks)->toHaveCount(3)
+ ->and($paymentCChecks[0]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentDChecks[1]->successful)
+ ->not
+ ->toBe(true)
+ ->and($paymentDChecks[2]->successful)
+ ->not
+ ->toBe(true);
+
+});