diff --git a/docs/core/extending/payments.md b/docs/core/extending/payments.md index 6338fb7cb4..e3792b382b 100644 --- a/docs/core/extending/payments.md +++ b/docs/core/extending/payments.md @@ -54,7 +54,7 @@ class CustomPayment extends AbstractPayment /** * {@inheritDoc} */ - public function authorize(): PaymentAuthorize + public function authorize(): ?PaymentAuthorize { if (!$this->order) { if (!$this->order = $this->cart->order) { diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index fcd578c740..e571d738f9 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -18,6 +18,18 @@ php artisan migrate Lunar currently provides bug fixes and security updates for only the latest minor release, e.g. `0.8`. +## 1.0.0-alpha.34 + +### Medium Impact + +#### Stripe Addon + +The Stripe driver will now check whether an order has a value for `placed_at` against an order and if so, no further processing will take place. + +Additionally, the logic in the webhook has been moved to the job queue, which is dispatched with a delay of 20 seconds, this is to allow storefronts to manually process a payment intent, in addition to the webhook, without having to worry about overlap. + +The Stripe webhook ENV entry has been changed from `STRIPE_WEBHOOK_PAYMENT_INTENT` to `LUNAR_STRIPE_WEBHOOK_SECRET` + ## 1.0.0-alpha.32 ### High Impact diff --git a/packages/core/database/migrations/2024_07_23_100000_add_fingerprint_to_orders_table.php b/packages/core/database/migrations/2024_07_23_100000_add_fingerprint_to_orders_table.php new file mode 100644 index 0000000000..2a3a7b9d80 --- /dev/null +++ b/packages/core/database/migrations/2024_07_23_100000_add_fingerprint_to_orders_table.php @@ -0,0 +1,25 @@ +prefix.'orders', function (Blueprint $table) { + $table->string('fingerprint')->nullable()->index(); + }); + } + + public function down(): void + { + Schema::table($this->prefix.'orders', function (Blueprint $table) { + $table->dropIndex(['fingerprint']); + }); + Schema::table($this->prefix.'orders', function (Blueprint $table) { + $table->dropColumn('fingerprint'); + }); + } +}; diff --git a/packages/core/src/Actions/Carts/CreateOrder.php b/packages/core/src/Actions/Carts/CreateOrder.php index 2ffc3f774d..f0276cef71 100644 --- a/packages/core/src/Actions/Carts/CreateOrder.php +++ b/packages/core/src/Actions/Carts/CreateOrder.php @@ -21,7 +21,7 @@ public function execute( ?int $orderIdToUpdate = null ): self { $this->passThrough = DB::transaction(function () use ($cart, $allowMultipleOrders, $orderIdToUpdate) { - $order = $cart->draftOrder($orderIdToUpdate)->first() ?: new Order; + $order = $cart->currentDraftOrder($orderIdToUpdate) ?: new Order; if ($cart->hasCompletedOrders() && ! $allowMultipleOrders) { throw new DisallowMultipleCartOrdersException; @@ -29,6 +29,7 @@ public function execute( $order->fill([ 'cart_id' => $cart->id, + 'fingerprint' => $cart->fingerprint(), ]); $order = app(Pipeline::class) diff --git a/packages/core/src/Actions/Carts/GenerateFingerprint.php b/packages/core/src/Actions/Carts/GenerateFingerprint.php index 1e52ea362d..548514bb71 100644 --- a/packages/core/src/Actions/Carts/GenerateFingerprint.php +++ b/packages/core/src/Actions/Carts/GenerateFingerprint.php @@ -10,18 +10,14 @@ class GenerateFingerprint public function execute(Cart $cart) { $value = $cart->lines->reduce(function (?string $carry, CartLine $line) { - $meta = $line->meta?->collect()->sortKeys()->toJson(); - return $carry. $line->purchasable_type. $line->purchasable_id. $line->quantity. - $meta. $line->subTotal; }); $value .= $cart->user_id.$cart->currency_id.$cart->coupon_code; - $value .= $cart->meta?->collect()->sortKeys()->toJson(); return sha1($value); } diff --git a/packages/core/src/Base/PaymentTypeInterface.php b/packages/core/src/Base/PaymentTypeInterface.php index efdc57e6a0..f1031ad0d7 100644 --- a/packages/core/src/Base/PaymentTypeInterface.php +++ b/packages/core/src/Base/PaymentTypeInterface.php @@ -35,10 +35,8 @@ public function setConfig(array $config): self; /** * Authorize the payment. - * - * @return void */ - public function authorize(): PaymentAuthorize; + public function authorize(): ?PaymentAuthorize; /** * Refund a transaction for a given amount. diff --git a/packages/core/src/Models/Cart.php b/packages/core/src/Models/Cart.php index eabb9ce91b..8bf9a3c546 100644 --- a/packages/core/src/Models/Cart.php +++ b/packages/core/src/Models/Cart.php @@ -293,6 +293,17 @@ public function draftOrder(?int $draftOrderId = null): HasOne })->whereNull('placed_at'); } + public function currentDraftOrder(?int $draftOrderId = null) + { + return $this->calculate() + ->draftOrder($draftOrderId) + ->where('fingerprint', $this->fingerprint()) + ->when( + $this->total, + fn (Builder $query, Price $price) => $query->where('total', $price->value) + )->first(); + } + /** * Return the completed order relationship. */ diff --git a/packages/core/src/PaymentTypes/OfflinePayment.php b/packages/core/src/PaymentTypes/OfflinePayment.php index e62490461e..ea3c68bec5 100644 --- a/packages/core/src/PaymentTypes/OfflinePayment.php +++ b/packages/core/src/PaymentTypes/OfflinePayment.php @@ -13,7 +13,7 @@ class OfflinePayment extends AbstractPayment /** * {@inheritDoc} */ - public function authorize(): PaymentAuthorize + public function authorize(): ?PaymentAuthorize { if (! $this->order) { if (! $this->order = $this->cart->draftOrder()->first()) { diff --git a/packages/stripe/README.md b/packages/stripe/README.md index 12b3e0b1d9..39f6ec0ac8 100644 --- a/packages/stripe/README.md +++ b/packages/stripe/README.md @@ -80,6 +80,9 @@ Make sure you have the Stripe credentials set in `config/services.php` 'stripe' => [ 'key' => env('STRIPE_SECRET'), 'public_key' => env('STRIPE_PK'), + 'webhooks' => [ + 'lunar' => env('LUNAR_STRIPE_WEBHOOK_SECRET'), + ], ], ``` @@ -224,9 +227,9 @@ Stripe::getCharges(string $paymentIntentId); ## Webhooks -The plugin provides a webhook you will need to add to Stripe. You can read the guide on how to do this on the Stripe website [https://stripe.com/docs/webhooks/quickstart](https://stripe.com/docs/webhooks/quickstart). +The add-on provides an optional webhook you may add to Stripe. You can read the guide on how to do this on the Stripe website [https://stripe.com/docs/webhooks/quickstart](https://stripe.com/docs/webhooks/quickstart). -The 3 events you should listen to are `payment_intent.payment_failed`,`payment_intent.processing`,`payment_intent.succeeded`. +The events you should listen to are `payment_intent.payment_failed`, `payment_intent.succeeded`. The path to the webhook will be `http:://yoursite.com/stripe/webhook`. @@ -248,6 +251,24 @@ return [ ]; ``` +If you do not wish to use the webhook, or would like to manually process an order as well, you are able to do so. + +```php +$cart = CartSession::current(); + +// With a draft order... +$draftOrder = $cart->createOrder(); +Payments::driver('stripe')->order($draftOrder)->withData([ + 'payment_intent' => $draftOrder->meta['payment_intent'], +])->authorize(); + +// Using just the cart... +Payments::driver('stripe')->cart($cart)->withData([ + 'payment_intent' => $cart->meta['payment_intent'], +])->authorize(); +``` + + ## Storefront Examples First we need to set up the backend API call to fetch or create the intent, this isn't Vue specific but will likely be different if you're using Livewire. diff --git a/packages/stripe/src/Http/Controllers/WebhookController.php b/packages/stripe/src/Http/Controllers/WebhookController.php index b0e78423f8..9eb83ee5c6 100644 --- a/packages/stripe/src/Http/Controllers/WebhookController.php +++ b/packages/stripe/src/Http/Controllers/WebhookController.php @@ -6,10 +6,8 @@ use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Log; -use Lunar\Events\PaymentAttemptEvent; -use Lunar\Facades\Payments; -use Lunar\Models\Cart; use Lunar\Stripe\Concerns\ConstructsWebhookEvent; +use Lunar\Stripe\Jobs\ProcessStripeWebhook; use Stripe\Exception\SignatureVerificationException; use Stripe\Exception\UnexpectedValueException; @@ -17,7 +15,7 @@ final class WebhookController extends Controller { public function __invoke(Request $request): JsonResponse { - $secret = config('services.stripe.webhooks.payment_intent'); + $secret = config('services.stripe.webhooks.lunar'); $stripeSig = $request->header('Stripe-Signature'); try { @@ -38,25 +36,9 @@ public function __invoke(Request $request): JsonResponse } $paymentIntent = $event->data->object->id; + $orderId = $event->data->object->metadata?->order_id; - $cart = Cart::where('meta->payment_intent', '=', $paymentIntent)->first(); - - if (! $cart) { - Log::error( - $error = "Unable to find cart with intent {$paymentIntent}" - ); - - return response()->json([ - 'webhook_successful' => false, - 'message' => $error, - ], 400); - } - - $payment = Payments::driver('stripe')->cart($cart->calculate())->withData([ - 'payment_intent' => $paymentIntent, - ])->authorize(); - - PaymentAttemptEvent::dispatch($payment); + ProcessStripeWebhook::dispatch($paymentIntent, $orderId)->delay(now()->addSeconds(20)); return response()->json([ 'webhook_successful' => true, diff --git a/packages/stripe/src/Http/Middleware/StripeWebhookMiddleware.php b/packages/stripe/src/Http/Middleware/StripeWebhookMiddleware.php index 309fb15422..07669da7f8 100644 --- a/packages/stripe/src/Http/Middleware/StripeWebhookMiddleware.php +++ b/packages/stripe/src/Http/Middleware/StripeWebhookMiddleware.php @@ -12,7 +12,7 @@ class StripeWebhookMiddleware { public function handle(Request $request, ?Closure $next = null) { - $secret = config('services.stripe.webhooks.payment_intent'); + $secret = config('services.stripe.webhooks.lunar'); $stripeSig = $request->header('Stripe-Signature'); try { @@ -28,10 +28,7 @@ public function handle(Request $request, ?Closure $next = null) if (! in_array( $event->type, [ - 'payment_intent.canceled', - 'payment_intent.created', 'payment_intent.payment_failed', - 'payment_intent.processing', 'payment_intent.succeeded', ] )) { diff --git a/packages/stripe/src/Jobs/ProcessStripeWebhook.php b/packages/stripe/src/Jobs/ProcessStripeWebhook.php new file mode 100644 index 0000000000..23a03b26d9 --- /dev/null +++ b/packages/stripe/src/Jobs/ProcessStripeWebhook.php @@ -0,0 +1,74 @@ +orderId) { + $order = Order::find($this->orderId); + + if ($order->placed_at) { + return; + } + } + + if (! $order) { + $cart = Cart::where('meta->payment_intent', '=', $this->paymentIntentId)->first(); + } + + if (! $cart && ! $order) { + Log::error( + "Unable to find cart with intent {$this->paymentIntentId}" + ); + + return; + } + + $payment = Payments::driver('stripe')->withData([ + 'payment_intent' => $this->paymentIntentId, + ]); + + if ($order) { + $payment->order($order)->authorize(); + + return; + } + + $payment->cart($cart->calculate())->authorize(); + } +} diff --git a/packages/stripe/src/Managers/StripeManager.php b/packages/stripe/src/Managers/StripeManager.php index 64b70a1c8c..f09d591b7e 100644 --- a/packages/stripe/src/Managers/StripeManager.php +++ b/packages/stripe/src/Managers/StripeManager.php @@ -98,8 +98,13 @@ public function updateIntent(Cart $cart, array $values): void return; } + $this->updateIntentById($meta['payment_intent'], $values); + } + + public function updateIntentById(string $id, array $values): void + { $this->getClient()->paymentIntents->update( - $meta['payment_intent'], + $id, $values ); } diff --git a/packages/stripe/src/StripePaymentType.php b/packages/stripe/src/StripePaymentType.php index c697b23c3b..0e873e2205 100644 --- a/packages/stripe/src/StripePaymentType.php +++ b/packages/stripe/src/StripePaymentType.php @@ -50,9 +50,13 @@ public function __construct() /** * Authorize the payment for processing. */ - final public function authorize(): PaymentAuthorize + final public function authorize(): ?PaymentAuthorize { - $this->order = $this->cart->draftOrder ?: $this->cart->completedOrder; + $this->order = $this->order ?: ($this->cart->draftOrder ?: $this->cart->completedOrder); + + if ($this->order && $this->order->placed_at) { + return null; + } if (! $this->order) { try { diff --git a/tests/core/Stubs/TestPaymentDriver.php b/tests/core/Stubs/TestPaymentDriver.php index a584a3be73..388e31475f 100644 --- a/tests/core/Stubs/TestPaymentDriver.php +++ b/tests/core/Stubs/TestPaymentDriver.php @@ -13,7 +13,7 @@ class TestPaymentDriver extends AbstractPayment /** * {@inheritDoc} */ - public function authorize(): PaymentAuthorize + public function authorize(): ?PaymentAuthorize { return new PaymentAuthorize(true); } diff --git a/tests/core/Unit/Actions/Carts/CreateOrderTest.php b/tests/core/Unit/Actions/Carts/CreateOrderTest.php index 7a0a07fc7d..c6933c96a5 100644 --- a/tests/core/Unit/Actions/Carts/CreateOrderTest.php +++ b/tests/core/Unit/Actions/Carts/CreateOrderTest.php @@ -216,16 +216,16 @@ function can_update_draft_order() 'tax_breakdown' => json_encode($breakdown), ]; - $cart = $cart->refresh(); + $cart = $cart->refresh()->calculate(); - expect($cart->draftOrder)->toBeInstanceOf(Order::class); - expect($order->cart_id)->toEqual($cart->id); - expect($cart->lines)->toHaveCount(1); - expect($order->lines)->toHaveCount(2); - expect($cart->addresses)->toHaveCount(2); - expect($order->addresses)->toHaveCount(2); - expect($order->shippingAddress)->toBeInstanceOf(OrderAddress::class); - expect($order->billingAddress)->toBeInstanceOf(OrderAddress::class); + expect($cart->currentDraftOrder())->toBeInstanceOf(Order::class) + ->and($order->cart_id)->toEqual($cart->id) + ->and($cart->lines)->toHaveCount(1) + ->and($order->lines)->toHaveCount(2) + ->and($cart->addresses)->toHaveCount(2) + ->and($order->addresses)->toHaveCount(2) + ->and($order->shippingAddress)->toBeInstanceOf(OrderAddress::class) + ->and($order->billingAddress)->toBeInstanceOf(OrderAddress::class); $this->assertDatabaseHas((new Order())->getTable(), $datacheck); $this->assertDatabaseHas((new OrderLine())->getTable(), [ @@ -349,7 +349,7 @@ function can_update_draft_order() 'tax_breakdown' => json_encode($breakdown), ]; - $cart = $cart->refresh(); + $cart = $cart->refresh()->calculate(); $this->assertDatabaseHas((new Order())->getTable(), $datacheck); }); diff --git a/tests/core/Unit/Models/CartTest.php b/tests/core/Unit/Models/CartTest.php index c19613abfc..3ad55a79eb 100644 --- a/tests/core/Unit/Models/CartTest.php +++ b/tests/core/Unit/Models/CartTest.php @@ -29,6 +29,8 @@ use Lunar\Models\TaxZonePostcode; use Lunar\Tests\Core\Stubs\User as StubUser; +use function Pest\Laravel\{assertDatabaseCount}; + uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); //function setAuthUserConfig() @@ -280,15 +282,17 @@ $draftOrder = Order::factory()->create([ 'cart_id' => $cart->id, + 'fingerprint' => $cart->calculate()->fingerprint(), + 'total' => $cart->calculate()->total->value, 'placed_at' => null, ]); - expect($cart->draftOrder->id)->toEqual($draftOrder->id); + expect($cart->currentDraftOrder()->id)->toEqual($draftOrder->id); $draftOrder->delete(); - expect($cart->draftOrder()->first())->toBeNull(); -}); + expect($cart->currentDraftOrder())->toBeNull(); +})->group('nooo'); test('can get cart draft order by id', function () { $currency = Currency::factory()->create(); @@ -307,15 +311,19 @@ $draftOrder = Order::factory()->create([ 'cart_id' => $cart->id, + 'fingerprint' => $cart->calculate()->fingerprint(), + 'total' => $cart->calculate()->total->value, 'placed_at' => null, ]); $draftOrderTwo = Order::factory()->create([ 'cart_id' => $cart->id, + 'fingerprint' => $cart->calculate()->fingerprint(), + 'total' => $cart->calculate()->total->value, 'placed_at' => null, ]); - expect($cart->draftOrder->id)->toEqual($draftOrder->id); + expect($cart->currentDraftOrder()->id)->toEqual($draftOrder->id); expect($cart->draftOrder($draftOrderTwo->id)->first()->id)->toEqual($draftOrderTwo->id); }); @@ -966,4 +974,172 @@ expect($cart->shippingOptionOverride)->toBeInstanceOf(ShippingOption::class); expect($shippingOption->identifier)->toEqual($cart->shippingOptionOverride->identifier); -})->group('foofoo'); +}); + +test('can get new draft order when cart changes', function () { + $currency = Currency::factory() + ->state([ + 'code' => 'USD', + ]) + ->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $taxClass = TaxClass::factory()->create(); + + // Add product with unit qty + $purchasable = ProductVariant::factory() + ->state([ + 'unit_quantity' => 1, + ]) + ->create(); + + Price::factory()->create([ + 'price' => 158, + 'min_quantity' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasable), + 'priceable_id' => $purchasable->id, + ]); + + CartAddress::factory()->create([ + 'type' => 'billing', + 'cart_id' => $cart->id, + ]); + + CartAddress::factory()->create([ + 'type' => 'shipping', + 'cart_id' => $cart->id, + ]); + + $option = new ShippingOption( + name: 'Basic Delivery', + description: 'Basic Delivery', + identifier: 'BASDEL', + price: new \Lunar\DataTypes\Price(500, $cart->currency, 1), + taxClass: $taxClass + ); + + ShippingManifest::addOption($option); + + $cart->setShippingOption($option); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 2, + ]); + + $order = $cart->createOrder(); + + assertDatabaseCount(Order::class, 1); + + expect($order->placed_at) + ->toBeNull() + ->and($order->fingerprint) + ->toBe($cart->fingerprint()) + ->and( + $cart->currentDraftOrder()->id + )->toBe($order->id); + + $cart->lines()->first()->update([ + 'quantity' => 5, + ]); + + $orderTwo = $cart->calculate()->createOrder(); + + assertDatabaseCount(Order::class, 2); + + expect($orderTwo->placed_at) + ->toBeNull() + ->and($orderTwo->fingerprint) + ->toBe($cart->fingerprint()) + ->and( + $cart->currentDraftOrder()->id + )->toBe($orderTwo->id); + +}); + +test('can get same draft order when cart does not change', function () { + $currency = Currency::factory() + ->state([ + 'code' => 'USD', + ]) + ->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $taxClass = TaxClass::factory()->create(); + + // Add product with unit qty + $purchasable = ProductVariant::factory() + ->state([ + 'unit_quantity' => 1, + ]) + ->create(); + + Price::factory()->create([ + 'price' => 158, + 'min_quantity' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasable), + 'priceable_id' => $purchasable->id, + ]); + + CartAddress::factory()->create([ + 'type' => 'billing', + 'cart_id' => $cart->id, + ]); + + CartAddress::factory()->create([ + 'type' => 'shipping', + 'cart_id' => $cart->id, + ]); + + $option = new ShippingOption( + name: 'Basic Delivery', + description: 'Basic Delivery', + identifier: 'BASDEL', + price: new \Lunar\DataTypes\Price(500, $cart->currency, 1), + taxClass: $taxClass + ); + + ShippingManifest::addOption($option); + + $cart->setShippingOption($option); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 2, + ]); + + $order = $cart->createOrder(); + + assertDatabaseCount(Order::class, 1); + + expect($order->placed_at) + ->toBeNull() + ->and($order->fingerprint) + ->toBe($cart->fingerprint()) + ->and( + $cart->currentDraftOrder()->first()->id + )->toBe($order->id); + + $newOrder = $cart->createOrder(); + + assertDatabaseCount(Order::class, 1); + + expect($newOrder->placed_at) + ->toBeNull() + ->and($newOrder->fingerprint) + ->toBe($cart->fingerprint()) + ->and( + $cart->currentDraftOrder()->id + )->toBe($newOrder->id); + +}); diff --git a/tests/opayo/Feature/OpayoPaymentTypeTest.php b/tests/opayo/Feature/OpayoPaymentTypeTest.php index c4049b3f39..f23fae2824 100644 --- a/tests/opayo/Feature/OpayoPaymentTypeTest.php +++ b/tests/opayo/Feature/OpayoPaymentTypeTest.php @@ -46,7 +46,7 @@ expect($cart->completedOrder()->first())->toBeNull() ->and($response->status)->toEqual(\Lunar\Opayo\Facades\Opayo::AUTH_FAILED) - ->and($cart->draftOrder()->first()) + ->and($cart->currentDraftOrder()) ->toBeInstanceOf(\Lunar\Models\Order::class); assertDatabaseHas(\Lunar\Models\Transaction::class, [ @@ -71,7 +71,7 @@ expect($cart->completedOrder()->first())->toBeNull() ->and($response->status)->toEqual(\Lunar\Opayo\Facades\Opayo::THREED_AUTH) - ->and($cart->draftOrder()->first()) + ->and($cart->currentDraftOrder()) ->toBeInstanceOf(\Lunar\Models\Order::class); }); @@ -90,9 +90,9 @@ expect($cart->completedOrder()->first())->toBeNull() ->and($response->status) ->toEqual(\Lunar\Opayo\Facades\Opayo::AUTH_FAILED) - ->and($cart->draftOrder()->first()) + ->and($cart->currentDraftOrder()) ->toBeInstanceOf(\Lunar\Models\Order::class) - ->and($cart->draftOrder()->first()->placed_at) + ->and($cart->currentDraftOrder()->first()->placed_at) ->toBeNull(); assertDatabaseHas(\Lunar\Models\Transaction::class, [ diff --git a/tests/stripe/TestCase.php b/tests/stripe/TestCase.php index bb931625c6..ea9d950826 100644 --- a/tests/stripe/TestCase.php +++ b/tests/stripe/TestCase.php @@ -22,7 +22,7 @@ protected function setUp(): void // additional setup Config::set('providers.users.model', User::class); Config::set('services.stripe.key', 'SK_TESTER'); - Config::set('services.stripe.webhooks.payment_intent', 'FOOBAR'); + Config::set('services.stripe.webhooks.lunar', 'FOOBAR'); activity()->disableLogging(); diff --git a/tests/stripe/Unit/StripePaymentTypeTest.php b/tests/stripe/Unit/StripePaymentTypeTest.php index 0ad8f4e9f6..b33e66aa64 100644 --- a/tests/stripe/Unit/StripePaymentTypeTest.php +++ b/tests/stripe/Unit/StripePaymentTypeTest.php @@ -38,19 +38,17 @@ 'payment_intent' => 'PI_FAIL', ])->authorize(); - $order = $cart->refresh()->draftOrder; - - expect($response)->toBeInstanceOf(PaymentAuthorize::class); - expect($response->success)->toBeFalse(); - expect($cart->refresh()->completedOrder)->toBeNull(); - expect($cart->refresh()->draftOrder)->not()->toBeNull(); + expect($response)->toBeInstanceOf(PaymentAuthorize::class) + ->and($response->success)->toBeFalse() + ->and($cart->refresh()->completedOrder)->toBeNull() + ->and($cart->currentDraftOrder())->not()->toBeNull(); assertDatabaseHas((new Transaction)->getTable(), [ - 'order_id' => $order->id, + 'order_id' => $cart->currentDraftOrder()->id, 'type' => 'capture', 'success' => false, ]); -}); +})->group('noo'); it('can retrieve existing payment intent', function () { $cart = CartBuilder::build([