Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak Stripe logic #1877

Merged
merged 26 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f3ce9ac
Tweak Stripe logic
alecritson Jul 22, 2024
8dbd5a5
Update ProcessStripeWebhook.php
alecritson Jul 22, 2024
19aba30
tweaks
alecritson Jul 22, 2024
44c9320
Remove duplicate events
alecritson Jul 22, 2024
2fa0faa
Update WebhookController.php
alecritson Jul 22, 2024
8c83418
Update WebhookController.php
alecritson Jul 22, 2024
337a561
Update WebhookController.php
alecritson Jul 22, 2024
9e11c5b
Update docs
alecritson Jul 23, 2024
0dd8329
Update upgrading.md
alecritson Jul 23, 2024
bd97a04
Update ProcessStripeWebhook.php
alecritson Jul 23, 2024
b9dccdd
Add fingerprint to orders
alecritson Jul 23, 2024
f2ac088
chore: fix code style
alecritson Jul 23, 2024
00aa430
Update CreateOrderTest.php
alecritson Jul 23, 2024
080f3ec
Merge branch 'feat/offload-stripe-to-queue' of github.com:lunarphp/lu…
alecritson Jul 23, 2024
97aa6b8
chore: fix code style
alecritson Jul 23, 2024
04a820a
Fixes
alecritson Jul 23, 2024
79a4d2f
Merge branch 'feat/offload-stripe-to-queue' of github.com:lunarphp/lu…
alecritson Jul 23, 2024
ce0c68d
chore: fix code style
alecritson Jul 23, 2024
67c252c
Update Cart.php
alecritson Jul 23, 2024
201de30
Merge branch 'feat/offload-stripe-to-queue' of github.com:lunarphp/lu…
alecritson Jul 23, 2024
892bde6
Config tweaks
alecritson Jul 23, 2024
a0c6421
Update Cart.php
alecritson Jul 23, 2024
fe71890
Tweaks
alecritson Jul 23, 2024
165dc4e
Update WebhookController.php
alecritson Jul 23, 2024
ace64dc
Update README.md
alecritson Jul 23, 2024
cc740e1
Tweaks
alecritson Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/core/extending/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions docs/core/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Lunar\Base\Migration;

return new class extends Migration
{
public function up(): void
{
Schema::table($this->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');
});
}
};
3 changes: 2 additions & 1 deletion packages/core/src/Actions/Carts/CreateOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ 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;
}

$order->fill([
'cart_id' => $cart->id,
'fingerprint' => $cart->fingerprint(),
]);

$order = app(Pipeline::class)
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/Actions/Carts/GenerateFingerprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/Base/PaymentTypeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/Models/Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/PaymentTypes/OfflinePayment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
25 changes: 23 additions & 2 deletions packages/stripe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
],
],
```

Expand Down Expand Up @@ -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`.

Expand All @@ -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.
Expand Down
26 changes: 4 additions & 22 deletions packages/stripe/src/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
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;

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 {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
]
)) {
Expand Down
74 changes: 74 additions & 0 deletions packages/stripe/src/Jobs/ProcessStripeWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Lunar\Stripe\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Lunar\Facades\Payments;
use Lunar\Models\Cart;
use Lunar\Models\Order;

class ProcessStripeWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Create a new job instance.
*
* @return void
*/
public function __construct(
public string $paymentIntentId,
public ?string $orderId
) {
//
}

/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// Do we have an order with this intent?
$cart = null;
$order = null;

if ($this->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();
}
}
7 changes: 6 additions & 1 deletion packages/stripe/src/Managers/StripeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/stripe/src/StripePaymentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
glennjacobs marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

if (! $this->order) {
try {
Expand Down
2 changes: 1 addition & 1 deletion tests/core/Stubs/TestPaymentDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class TestPaymentDriver extends AbstractPayment
/**
* {@inheritDoc}
*/
public function authorize(): PaymentAuthorize
public function authorize(): ?PaymentAuthorize
{
return new PaymentAuthorize(true);
}
Expand Down
20 changes: 10 additions & 10 deletions tests/core/Unit/Actions/Carts/CreateOrderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(), [
Expand Down Expand Up @@ -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);
});
Loading
Loading