Skip to content

Commit

Permalink
Tweak Stripe logic (#1877)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecritson authored Jul 23, 2024
1 parent 747ccb4 commit 7d2fc35
Show file tree
Hide file tree
Showing 20 changed files with 370 additions and 70 deletions.
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) {
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

0 comments on commit 7d2fc35

Please sign in to comment.