where('code', 'DE')->first() ?? Country::query()->forceCreate([ 'code' => 'DE', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Germanija', ]); } function apilogMakeUser(int $adminLevel = 7): User { $user = User::query()->create([ 'email' => uniqid('apilog_', true).'@test.example', 'password' => bcrypt('password'), ]); $account = UserAccount::query()->create([ 'm_first_name' => 'Api', 'm_last_name' => 'Partner', 'country_id' => apilogCountry()->id, ]); $user->forceFill([ 'admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false, 'account_id' => $account->id, ])->save(); return $user->fresh(); } function apilogMakeShoppingUser(int $wpOrderNumber): ShoppingUser { $country = apilogCountry(); $shipping = Shipping::query()->forceCreate(['name' => 'Standard']); ShippingCountry::query()->forceCreate([ 'shipping_id' => $shipping->id, 'country_id' => $country->id, ]); return ShoppingUser::query()->forceCreate([ 'billing_country_id' => $country->id, 'shipping_country_id' => $country->id, 'billing_firstname' => 'Api', 'billing_lastname' => 'Kunde', 'wp_order_number' => $wpOrderNumber, 'member_id' => apilogMakeUser(0)->id, 'auth_user_id' => apilogMakeUser(0)->id, ]); } function apilogMakeProduct(string $name, int $wpNumber, string $price = '19,90'): Product { return Product::query()->create([ 'name' => $name, 'title' => $name, 'active' => true, 'is_set' => false, 'wp_number' => $wpNumber, 'price' => $price, 'tax' => '19', 'weight' => 100, ]); } test('uebertragung mit unbekannter sku wird protokolliert und markiert', function () { Mail::fake(); Passport::actingAs(apilogMakeUser()); $product = apilogMakeProduct('Apilog Creme', 100); $shoppingUser = apilogMakeShoppingUser(4711); $response = $this->postJson('https://api.'.config('app.api_domain').'/wp/order', [ 'wp_order_number' => 4711, 'wp_order' => [ ['article' => 100, 'qty' => 2, 'price' => 1990], ['article' => 999, 'qty' => 1, 'price' => 500], ], ]); $response->assertOk()->assertJsonPath('success', true); $log = ApiOrderLog::query()->sole(); expect($log->has_errors)->toBeTrue() ->and($log->error_summary)->toBe('1 von 2 Positionen ohne Produktzuordnung') ->and($log->wp_order_number)->toBe(4711) ->and($log->shopping_user_id)->toBe($shoppingUser->id) ->and($log->shopping_order_id)->not->toBeNull() ->and($log->payload['lines'])->toHaveCount(2) ->and($log->payload['lines'][0]['product_id'])->toBe($product->id) ->and($log->payload['lines'][0]['failed'])->toBeFalse() ->and($log->payload['lines'][1]['product_id'])->toBeNull() ->and($log->payload['lines'][1]['failed'])->toBeTrue() ->and($log->payload['lines'][1]['messages'])->toContain('article not found'); // Die nicht zugeordnete Position fehlt in der Bestellung (Bestand: kein Abzug möglich) expect(ShoppingOrderItem::query()->where('shopping_order_id', $log->shopping_order_id)->count())->toBe(1); }); test('bekannte sku ohne menge wird als fehler markiert (position fehlt in der bestellung)', function () { // Review 20.07.2026: vorher hing `failed` nur am SKU-Lookup — eine Zeile mit // gültigem Artikel, aber fehlender Menge/Preis wurde verworfen und trotzdem // grün protokolliert. Mail::fake(); Passport::actingAs(apilogMakeUser()); $product = apilogMakeProduct('Apilog Öl', 300); apilogMakeProduct('Apilog Balsam', 301); apilogMakeShoppingUser(4715); $this->postJson('https://api.'.config('app.api_domain').'/wp/order', [ 'wp_order_number' => 4715, 'wp_order' => [ ['article' => 300, 'price' => 1990], // qty fehlt — wird nicht übernommen ['article' => 301, 'qty' => 1, 'price' => 1990], ], ])->assertOk(); $log = ApiOrderLog::query()->sole(); expect($log->has_errors)->toBeTrue() ->and($log->error_summary)->toBe('1 von 2 Positionen ohne Produktzuordnung') ->and($log->payload['lines'][0]['failed'])->toBeTrue() ->and($log->payload['lines'][0]['messages'])->toContain('article parameter is missing') ->and($log->payload['lines'][1]['failed'])->toBeFalse(); // Nur die vollständige Position landet in der Bestellung expect(ShoppingOrderItem::query()->where('shopping_order_id', $log->shopping_order_id)->count())->toBe(1) ->and(ShoppingOrderItem::query()->where('product_id', $product->id)->count())->toBe(0); }); test('fehlerfreie uebertragung wird als ok protokolliert', function () { Mail::fake(); Passport::actingAs(apilogMakeUser()); apilogMakeProduct('Apilog Seife', 200); apilogMakeShoppingUser(4712); $this->postJson('https://api.'.config('app.api_domain').'/wp/order', [ 'wp_order_number' => 4712, 'wp_order' => [ ['article' => 200, 'qty' => 1, 'price' => 1990], ], ])->assertOk(); $log = ApiOrderLog::query()->sole(); expect($log->has_errors)->toBeFalse() ->and($log->error_summary)->toBeNull() ->and($log->shopping_order_id)->not->toBeNull(); }); test('bereits existierende bestellung wird als fehler protokolliert', function () { Mail::fake(); Passport::actingAs(apilogMakeUser()); $shoppingUser = apilogMakeShoppingUser(4713); ShoppingOrder::query()->forceCreate([ 'shopping_user_id' => $shoppingUser->id, 'auth_user_id' => apilogMakeUser(0)->id, 'country_id' => ShippingCountry::query()->first()->id, 'shipped' => 0, ]); $this->postJson('https://api.'.config('app.api_domain').'/wp/order', [ 'wp_order_number' => 4713, 'wp_order' => [['article' => 1, 'qty' => 1, 'price' => 100]], ])->assertStatus(400); $log = ApiOrderLog::query()->sole(); expect($log->has_errors)->toBeTrue() ->and($log->error_summary)->toContain('existiert bereits'); }); test('unbekannte wp-bestellnummer wird protokolliert', function () { Mail::fake(); Passport::actingAs(apilogMakeUser()); $this->postJson('https://api.'.config('app.api_domain').'/wp/order', [ 'wp_order_number' => 9999, 'wp_order' => [['article' => 1, 'qty' => 1, 'price' => 100]], ])->assertStatus(400); $log = ApiOrderLog::query()->sole(); expect($log->has_errors)->toBeTrue() ->and($log->shopping_user_id)->toBeNull() ->and($log->wp_order_number)->toBe(9999); }); test('uebersicht rendert eintraege mit fehler-markierung', function () { $shoppingUser = apilogMakeShoppingUser(4714); ApiOrderLog::query()->create([ 'shopping_user_id' => $shoppingUser->id, 'wp_order_number' => 4714, 'endpoint' => 'order', 'payload' => ['lines' => [['article' => 5, 'qty' => 1, 'price' => 100, 'product_id' => null, 'product_name' => null, 'messages' => ['article not found'], 'failed' => true]]], 'has_errors' => true, 'error_summary' => '1 von 1 Positionen ohne Produktzuordnung', ]); $this->actingAs(apilogMakeUser(1)) ->get(route('admin.inventory.api-orders.index')) ->assertOk() ->assertSee('API-Bestellungen') ->assertSee('4714') ->assertSee('1 von 1 Positionen ohne Produktzuordnung'); }); test('doppelte wp-nummern aktiver produkte werden gewarnt', function () { apilogMakeProduct('Apilog Duplikat A', 300); apilogMakeProduct('Apilog Duplikat B', 300); $this->actingAs(apilogMakeUser(1)) ->get(route('admin.inventory.api-orders.index')) ->assertOk() ->assertSee('Doppelte WP-Nummern') ->assertSee('Apilog Duplikat A') ->assertSee('Apilog Duplikat B'); }); test('produkte ohne wp-nummer werden gezaehlt', function () { apilogMakeProduct('Apilog Mit Nummer', 400); Product::query()->create([ 'name' => 'Apilog Ohne Nummer', 'title' => 'Apilog Ohne Nummer', 'active' => true, 'is_set' => false, ]); $this->actingAs(apilogMakeUser(1)) ->get(route('admin.inventory.api-orders.index')) ->assertOk() ->assertSee('aktive Produkte ohne WP-Nummer', false) ->assertSee('Apilog Ohne Nummer'); }); test('ohne copyreader-rechte kein zugriff', function () { $this->actingAs(apilogMakeUser(0)) ->get(route('admin.inventory.api-orders.index')) ->assertRedirect(); });