278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
||
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Http\Client\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Facades\Schema;
|
||
|
||
uses(Tests\TestCase::class);
|
||
|
||
beforeEach(function () {
|
||
Schema::dropIfExists('trans_products');
|
||
Schema::dropIfExists('products');
|
||
Schema::dropIfExists('trans_ingredients');
|
||
Schema::dropIfExists('ingredients');
|
||
Schema::dropIfExists('dashboard_news');
|
||
Schema::dropIfExists('trans_languages');
|
||
|
||
Schema::create('trans_languages', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('language');
|
||
$table->string('name')->nullable();
|
||
$table->timestamps();
|
||
});
|
||
|
||
Schema::create('products', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('name');
|
||
$table->text('copy')->nullable();
|
||
$table->text('description')->nullable();
|
||
$table->text('usage')->nullable();
|
||
$table->text('ingredients')->nullable();
|
||
});
|
||
|
||
Schema::create('trans_products', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('language');
|
||
$table->unsignedBigInteger('product_id');
|
||
$table->string('key')->nullable();
|
||
$table->text('value')->nullable();
|
||
$table->timestamps();
|
||
});
|
||
|
||
Schema::create('ingredients', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('name');
|
||
$table->text('inci')->nullable();
|
||
$table->text('effect')->nullable();
|
||
});
|
||
|
||
Schema::create('trans_ingredients', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('language');
|
||
$table->unsignedBigInteger('ingredient_id');
|
||
$table->string('key')->nullable();
|
||
$table->text('value')->nullable();
|
||
$table->timestamps();
|
||
});
|
||
|
||
Schema::create('dashboard_news', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('title')->nullable();
|
||
$table->text('teaser')->nullable();
|
||
$table->text('content')->nullable();
|
||
$table->json('trans_title')->nullable();
|
||
$table->json('trans_teaser')->nullable();
|
||
$table->json('trans_content')->nullable();
|
||
$table->boolean('active')->default(true);
|
||
$table->timestamps();
|
||
});
|
||
});
|
||
|
||
it('backfills missing french product translations without overwriting existing values', function () {
|
||
DB::table('products')->insert([
|
||
'id' => 1,
|
||
'name' => 'Aloe Vera Gel',
|
||
'copy' => 'Deutsche Kurzbeschreibung',
|
||
'description' => 'Deutsche Beschreibung',
|
||
'usage' => null,
|
||
'ingredients' => 'Aloe Vera',
|
||
]);
|
||
|
||
DB::table('trans_products')->insert([
|
||
'language' => 'fr',
|
||
'product_id' => 1,
|
||
'key' => 'name',
|
||
'value' => 'Nom existant',
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--driver' => 'copy-source',
|
||
'--models' => 'products',
|
||
])
|
||
->expectsOutput('Processing products...')
|
||
->expectsOutput('Status products: 1 Datensätze, 5 Felder.')
|
||
->expectsOutput('Datensatz 1/1: products#1')
|
||
->expectsOutput(' - products#1.name: vorhandene Übersetzung, übersprungen.')
|
||
->expectsOutput(' - products#1.copy: übernehme Quelle...')
|
||
->expectsOutput(' - products#1.copy: gespeichert (erstellt).')
|
||
->expectsOutput(' - products#1.usage: Quelle leer, übersprungen.')
|
||
->assertSuccessful();
|
||
|
||
expect(DB::table('trans_languages')->where('language', 'fr')->value('name'))->toBe('Französisch')
|
||
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'name')->value('value'))->toBe('Nom existant')
|
||
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'copy')->value('value'))->toBe('Deutsche Kurzbeschreibung')
|
||
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'description')->value('value'))->toBe('Deutsche Beschreibung')
|
||
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'usage')->exists())->toBeFalse();
|
||
});
|
||
|
||
it('uses openai for translatable fields and copies inci values unchanged', function () {
|
||
config()->set('services.openai.api_key', 'test-key');
|
||
config()->set('services.openai.model', 'gpt-5.4-mini');
|
||
|
||
Http::fake(function (Request $request) {
|
||
$text = $request['messages'][1]['content'];
|
||
|
||
return Http::response([
|
||
'choices' => [
|
||
[
|
||
'message' => [
|
||
'content' => str_contains($text, 'Pflege')
|
||
? 'FR __MIVITA_TRANSLATION_TOKEN_1__ __MIVITA_TRANSLATION_TOKEN_0__ Pflege'
|
||
: 'FR Beruhigende Wirkung',
|
||
],
|
||
],
|
||
],
|
||
]);
|
||
});
|
||
|
||
DB::table('ingredients')->insert([
|
||
'id' => 1,
|
||
'name' => 'MIVITA :amount Pflege',
|
||
'inci' => 'Aloe Barbadensis Leaf Juice',
|
||
'effect' => 'Beruhigende Wirkung',
|
||
]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--driver' => 'openai',
|
||
'--models' => 'ingredients',
|
||
])->assertSuccessful();
|
||
|
||
expect(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'name')->value('value'))->toBe('FR MIVITA :amount Pflege')
|
||
->and(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'inci')->value('value'))->toBe('Aloe Barbadensis Leaf Juice')
|
||
->and(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'effect')->value('value'))->toBe('FR Beruhigende Wirkung');
|
||
|
||
Http::assertSentCount(2);
|
||
Http::assertSent(fn (Request $request): bool => $request->hasHeader('Authorization', 'Bearer test-key')
|
||
&& $request->url() === 'https://api.openai.com/v1/chat/completions'
|
||
&& $request['model'] === 'gpt-5.4-mini');
|
||
});
|
||
|
||
it('backfills dashboard news json translations', function () {
|
||
DB::table('dashboard_news')->insert([
|
||
'id' => 1,
|
||
'title' => 'Neue Aktion',
|
||
'teaser' => 'Kurzer Hinweis für das Dashboard',
|
||
'content' => '<p>Ausführlicher Inhalt für Berater.</p>',
|
||
'trans_title' => json_encode(['en' => 'Existing English title']),
|
||
'trans_teaser' => json_encode(['fr' => 'Teaser existant']),
|
||
'trans_content' => null,
|
||
'active' => true,
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--driver' => 'copy-source',
|
||
'--models' => 'dashboard_news',
|
||
])
|
||
->expectsOutput('Processing dashboard_news...')
|
||
->expectsOutput('Status dashboard_news: 1 Datensätze, 3 Felder.')
|
||
->expectsOutput('Datensatz 1/1: dashboard_news#1')
|
||
->expectsOutput(' - dashboard_news#1.title: übernehme Quelle...')
|
||
->expectsOutput(' - dashboard_news#1.title: gespeichert (erstellt).')
|
||
->expectsOutput(' - dashboard_news#1.teaser: vorhandene Übersetzung, übersprungen.')
|
||
->expectsOutput(' - dashboard_news#1.content: übernehme Quelle...')
|
||
->expectsOutput(' - dashboard_news#1.content: gespeichert (erstellt).')
|
||
->assertSuccessful();
|
||
|
||
$news = DB::table('dashboard_news')->where('id', 1)->first();
|
||
|
||
expect(json_decode($news->trans_title, true))->toMatchArray([
|
||
'en' => 'Existing English title',
|
||
'fr' => 'Neue Aktion',
|
||
])
|
||
->and(json_decode($news->trans_teaser, true))->toMatchArray([
|
||
'fr' => 'Teaser existant',
|
||
])
|
||
->and(json_decode($news->trans_content, true))->toMatchArray([
|
||
'fr' => '<p>Ausführlicher Inhalt für Berater.</p>',
|
||
]);
|
||
});
|
||
|
||
it('updates dashboard news json translations when overwrite is enabled', function () {
|
||
DB::table('dashboard_news')->insert([
|
||
'id' => 1,
|
||
'title' => 'Aktualisierte Aktion',
|
||
'teaser' => null,
|
||
'content' => null,
|
||
'trans_title' => json_encode(['fr' => 'Ancien titre']),
|
||
'trans_teaser' => null,
|
||
'trans_content' => null,
|
||
'active' => true,
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--driver' => 'copy-source',
|
||
'--models' => 'dashboard_news',
|
||
'--overwrite' => true,
|
||
])
|
||
->expectsOutput(' - dashboard_news#1.title: gespeichert (aktualisiert).')
|
||
->assertSuccessful();
|
||
|
||
$translations = json_decode(DB::table('dashboard_news')->where('id', 1)->value('trans_title'), true);
|
||
|
||
expect($translations['fr'])->toBe('Aktualisierte Aktion');
|
||
});
|
||
|
||
it('prints sample translations for the api test mode', function () {
|
||
config()->set('services.openai.api_key', 'test-key');
|
||
config()->set('services.openai.model', 'gpt-5.4-mini');
|
||
|
||
Http::fakeSequence()
|
||
->push(['choices' => [['message' => ['content' => 'Gel d’Aloe Vera pour les soins quotidiens de la peau.']]]])
|
||
->push(['choices' => [['message' => ['content' => 'Le conseiller peut recommander un abonnement adapté à son client.']]]])
|
||
->push(['choices' => [['message' => ['content' => 'Description du produit MIVITA avec :amount ml de contenu et paiement PayPal.']]]]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--test-api' => true,
|
||
])
|
||
->expectsOutput('OpenAI translation API test')
|
||
->expectsOutput('Model: gpt-5.4-mini')
|
||
->expectsOutput('Language: de -> fr')
|
||
->expectsOutput('[1] DE: Aloe Vera Gel für die tägliche Pflege der Haut.')
|
||
->expectsOutput('[1] FR: Gel d’Aloe Vera pour les soins quotidiens de la peau.')
|
||
->expectsOutput('[2] DE: Der Berater kann seinem Kunden ein passendes Abo empfehlen.')
|
||
->expectsOutput('[2] FR: Le conseiller peut recommander un abonnement adapté à son client.')
|
||
->expectsOutput('[3] DE: MIVITA Produktbeschreibung mit :amount ml Inhalt und PayPal Zahlung.')
|
||
->expectsOutput('[3] FR: Description du produit MIVITA avec :amount ml de contenu et paiement PayPal.')
|
||
->expectsOutput('API test completed.')
|
||
->assertSuccessful();
|
||
|
||
Http::assertSentCount(3);
|
||
});
|
||
|
||
it('prints a helpful message when openai quota is exceeded', function () {
|
||
config()->set('services.openai.api_key', 'test-key');
|
||
config()->set('services.openai.model', 'gpt-5.4-mini');
|
||
|
||
Http::fake([
|
||
'*' => Http::response([
|
||
'error' => [
|
||
'message' => 'You exceeded your current quota, please check your plan and billing details.',
|
||
'type' => 'insufficient_quota',
|
||
'code' => 'insufficient_quota',
|
||
],
|
||
], 429),
|
||
]);
|
||
|
||
$this->artisan('translation:backfill-french-db', [
|
||
'--test-api' => true,
|
||
])
|
||
->expectsOutput('OpenAI translation API test')
|
||
->expectsOutput('Model: gpt-5.4-mini')
|
||
->expectsOutput('Language: de -> fr')
|
||
->expectsOutput('OpenAI API request failed with HTTP 429.')
|
||
->expectsOutput('Code: insufficient_quota')
|
||
->expectsOutput('Type: insufficient_quota')
|
||
->expectsOutput('Message: You exceeded your current quota, please check your plan and billing details.')
|
||
->expectsOutput('Bitte prüfe im OpenAI Dashboard das Billing, das Projekt-Budget, Usage-Limits und ob der OPENAI_API_KEY zum richtigen Projekt gehört.')
|
||
->assertFailed();
|
||
|
||
Http::assertSentCount(1);
|
||
});
|