From e14baca7e09e7f1aea81d9e641db14d858d4340b Mon Sep 17 00:00:00 2001 From: Malek Tellissi Date: Sat, 12 Sep 2026 16:43:48 +0200 Subject: [PATCH] Add LOT API v1 endpoint with full batch details Route changed to /api/v1/{lot_number}. LotResource now includes packing details, units_produced, quantity_g and recipe version. CORS config added for public API access. README documents the endpoint, example response and subdomain setup instructions. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 75 ++++++++++++++++++++++ app/Http/Controllers/Api/LotController.php | 1 + app/Http/Resources/LotResource.php | 17 +++-- config/cors.php | 19 ++++++ routes/api.php | 4 +- 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 config/cors.php diff --git a/README.md b/README.md index abe4612..beda6af 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,80 @@ Rezepte sind versioniert und einem Produkt zugeordnet: --- +## LOT-API + +Öffentliche JSON-API zur Rückverfolgung von Chargen. Kein Auth erforderlich. + +### Endpunkt + +``` +GET https://api.sms.spices.blue/api/v1/{lot_number} +``` + +### Beispiel-Antwort + +```json +{ + "data": { + "lot_number": "LOT-2024-001", + "produced_at": "2024-10-01", + "expires_at": "2026-10-01", + "units_produced": 50, + "quantity_g": 5000, + "product": { + "name": "Curry Klassik", + "description": "..." + }, + "packing": { + "name": "Standbodenbeutel", + "size_g": 100, + "sku": "CUR-100", + "gtin": "4012345678901" + }, + "recipe": { + "version": 3, + "created_at": "2024-09-15" + }, + "spices": [ + { + "name": "Kurkuma", + "percentage": 60.0, + "origin": { + "country": "Indien", + "farmer": "Singh Farm", + "supplier": "Gewürzhandel GmbH", + "notes": null + } + } + ] + } +} +``` + +Fehler (nicht gefunden): +```json +{ "error": "LOT-Nummer nicht gefunden." } +``` + +### Subdomain-Setup auf dem Server + +Der Container läuft intern auf `127.0.0.1:5000`. Der Host-Proxy (Nginx/Caddy) muss `api.sms.spices.blue` auf denselben Port zeigen — Beispiel für Nginx: + +```nginx +server { + server_name api.sms.spices.blue; + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +Danach SSL via `certbot --nginx -d api.sms.spices.blue` erneuern. + +--- + ## Cronjob Auf dem Server einrichten, damit der Laravel-Scheduler läuft: @@ -198,4 +272,5 @@ Table batch_origins { - [x] Shopify-Import (Produkte, Varianten, Lagerbestand, Bilder, Beschreibung, Preise) - [x] Continuous Deployment via Gitea Actions + Docker - [x] Frontend (Sidebar, Navigation, Barcode-Preview, Bild-Grid) +- [x] LOT-API (JSON, öffentlich, Subdomain api.sms.spices.blue) - [ ] Dashboard (Füllstände, Lager, Sales-Zeitspanne) diff --git a/app/Http/Controllers/Api/LotController.php b/app/Http/Controllers/Api/LotController.php index 7b2c730..98c1778 100644 --- a/app/Http/Controllers/Api/LotController.php +++ b/app/Http/Controllers/Api/LotController.php @@ -13,6 +13,7 @@ class LotController extends Controller { $batch = Batch::with([ 'product', + 'size.packing', 'recipe.spices', 'origins.spice', 'origins.supplier', diff --git a/app/Http/Resources/LotResource.php b/app/Http/Resources/LotResource.php index e9c6465..78a2b22 100644 --- a/app/Http/Resources/LotResource.php +++ b/app/Http/Resources/LotResource.php @@ -10,15 +10,24 @@ class LotResource extends JsonResource public function toArray(Request $request): array { return [ - 'lot_number' => $this->lot_number, - 'produced_at' => $this->produced_at?->toDateString(), - 'expires_at' => $this->expires_at?->toDateString(), + 'lot_number' => $this->lot_number, + 'produced_at' => $this->produced_at?->toDateString(), + 'expires_at' => $this->expires_at?->toDateString(), + 'units_produced' => $this->units_produced, + 'quantity_g' => (float) $this->quantity_g, 'product' => [ 'name' => $this->product->title, 'description' => $this->product->description, ], + 'packing' => $this->size ? [ + 'name' => $this->size->packing->name, + 'size_g' => (float) $this->size->packing->size, + 'sku' => $this->size->sku, + 'gtin' => $this->size->gtin, + ] : null, 'recipe' => [ - 'name' => $this->recipe->name, + 'version' => $this->recipe->version, + 'created_at' => $this->recipe->created_at->toDateString(), ], 'spices' => $this->recipe->spices->map(function ($spice) { $origin = $this->origins->firstWhere('spice_id', $spice->id); diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..e774039 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,19 @@ + ['api/*'], + + 'allowed_methods' => ['GET'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['Content-Type', 'Accept'], + + 'exposed_headers' => [], + + 'max_age' => 86400, + + 'supports_credentials' => false, +]; diff --git a/routes/api.php b/routes/api.php index 364c0cc..ed76452 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,8 +5,8 @@ use App\Http\Controllers\ShopifyWebhookController; use App\Http\Middleware\VerifyShopifyWebhook; use Illuminate\Support\Facades\Route; -Route::get('/lot/{lot_number}', [LotController::class, 'show']) - ->middleware('throttle:30,1') +Route::get('/v1/{lot_number}', [LotController::class, 'show']) + ->middleware('throttle:60,1') ->name('api.lot'); Route::post('/webhooks/shopify/orders-paid', [ShopifyWebhookController::class, 'ordersPaid'])