recordTransfer() löste die SKU erneut auf und markierte nur unbekannte Artikel als Fehler — Zeilen mit gültiger SKU aber fehlender Menge/Preis wurden verworfen und trotzdem grün protokolliert. prepareOrder() reicht jetzt included/product_id/product_name durch; failed = Position nicht übernommen. Entfernt zugleich die N+1-Produkt-Queries je Position. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
259 lines
9.1 KiB
PHP
259 lines
9.1 KiB
PHP
<?php
|
|
|
|
use App\Models\ApiOrderLog;
|
|
use App\Models\Country;
|
|
use App\Models\Product;
|
|
use App\Models\Shipping;
|
|
use App\Models\ShippingCountry;
|
|
use App\Models\ShoppingOrder;
|
|
use App\Models\ShoppingOrderItem;
|
|
use App\Models\ShoppingUser;
|
|
use App\Models\UserAccount;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Laravel\Passport\Passport;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function apilogCountry(): Country
|
|
{
|
|
return Country::query()->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();
|
|
});
|