Add Shopify inventory sync and order webhook
Deploy / deploy (push) Successful in 14s

- Migration: shopify_variant_id on products_sizes, shopify_order_id on products_sales
- ShopifyService: paginated REST client (2024-10 API)
- shopify:map — one-time command to link variants by SKU
- shopify:sync — reconciliation command (Shopify is master)
- VerifyShopifyWebhook middleware: HMAC-SHA256 verification
- ShopifyWebhookController: idempotent orders/paid handler, FIFO inventory deduction
- Webhook route: POST /api/webhooks/shopify/orders-paid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Malek Tellissi
2026-09-11 02:53:49 +02:00
co-authored by Claude Sonnet 4.6
parent f54b471848
commit 467d85dfec
11 changed files with 349 additions and 1 deletions
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\ProductInventory;
use App\Models\ProductSale;
use App\Models\ProductSize;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ShopifyWebhookController extends Controller
{
public function ordersPaid(Request $request): Response
{
$order = $request->json()->all();
$orderId = (string) ($order['id'] ?? '');
// Idempotency: ignore already-processed orders
if (ProductSale::where('shopify_order_id', $orderId)->exists()) {
return response('', 200);
}
DB::transaction(function () use ($order, $orderId) {
foreach ($order['line_items'] ?? [] as $item) {
$variantId = (string) ($item['variant_id'] ?? '');
$quantity = (int) ($item['quantity'] ?? 0);
if (!$variantId || $quantity <= 0) {
continue;
}
$size = ProductSize::where('shopify_variant_id', $variantId)->first();
if (!$size) {
Log::warning('Shopify webhook: unbekannte Variant-ID', [
'variant_id' => $variantId,
'order_id' => $orderId,
]);
continue;
}
// FIFO: älteste verfügbare Einheiten ausbuchen
ProductInventory::where('size_id', $size->id)
->whereNull('out_at')
->oldest()
->limit($quantity)
->update(['out_at' => now()]);
ProductSale::create([
'product_id' => $size->product_id,
'size_id' => $size->id,
'amount' => $quantity,
'sold_at' => now(),
'shopify_order_id' => $orderId,
]);
}
});
return response('', 200);
}
}