mivita/tests/Feature/BackfillFrenchDatabaseTranslationsTest.php

278 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 dAloe 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 dAloe 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);
});