Route languages
This commit is contained in:
parent
f765d04d06
commit
2087ca389e
15 changed files with 1279 additions and 322 deletions
62
backend/app/Http/Controllers/Api/V1/LanguagesController.php
Normal file
62
backend/app/Http/Controllers/Api/V1/LanguagesController.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class LanguagesController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get languages available for route creation
|
||||||
|
* Returns languages that have both active feeds and active channels
|
||||||
|
*/
|
||||||
|
public function availableForRoutes(): JsonResponse
|
||||||
|
{
|
||||||
|
$languages = Language::availableForRoutes()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'short_code', 'name', 'native_name']);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$languages,
|
||||||
|
'Available languages for routes retrieved successfully.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get feeds filtered by language
|
||||||
|
*/
|
||||||
|
public function feedsByLanguage(int $languageId): JsonResponse
|
||||||
|
{
|
||||||
|
$language = Language::findOrFail($languageId);
|
||||||
|
|
||||||
|
$feeds = $language->feeds()
|
||||||
|
->where('feeds.is_active', true)
|
||||||
|
->where('feed_languages.is_active', true)
|
||||||
|
->with('languages')
|
||||||
|
->get(['feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider']);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$feeds,
|
||||||
|
"Feeds for language '{$language->name}' retrieved successfully."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get channels filtered by language
|
||||||
|
*/
|
||||||
|
public function channelsByLanguage(int $languageId): JsonResponse
|
||||||
|
{
|
||||||
|
$language = Language::findOrFail($languageId);
|
||||||
|
|
||||||
|
$channels = $language->platformChannels()
|
||||||
|
->where('platform_channels.is_active', true)
|
||||||
|
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
|
||||||
|
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
|
||||||
|
|
||||||
|
return $this->sendResponse(
|
||||||
|
$channels,
|
||||||
|
"Channels for language '{$language->name}' retrieved successfully."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -91,13 +91,19 @@ public function options(): JsonResponse
|
||||||
|
|
||||||
// Get existing feeds and channels for route creation
|
// Get existing feeds and channels for route creation
|
||||||
$feeds = Feed::where('is_active', true)
|
$feeds = Feed::where('is_active', true)
|
||||||
|
->with('languages')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name', 'url', 'type']);
|
->get(['id', 'name', 'url', 'type']);
|
||||||
|
|
||||||
$platformChannels = PlatformChannel::where('is_active', true)
|
$platformChannels = PlatformChannel::where('is_active', true)
|
||||||
->with(['platformInstance:id,name,url'])
|
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
|
||||||
|
|
||||||
|
// Get languages available for routes (have both feeds and channels)
|
||||||
|
$availableLanguages = Language::availableForRoutes()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'short_code', 'name', 'native_name']);
|
||||||
|
|
||||||
// Get feed providers from config
|
// Get feed providers from config
|
||||||
$feedProviders = collect(config('feed.providers', []))
|
$feedProviders = collect(config('feed.providers', []))
|
||||||
|
|
@ -109,6 +115,7 @@ public function options(): JsonResponse
|
||||||
'platform_instances' => $platformInstances,
|
'platform_instances' => $platformInstances,
|
||||||
'feeds' => $feeds,
|
'feeds' => $feeds,
|
||||||
'platform_channels' => $platformChannels,
|
'platform_channels' => $platformChannels,
|
||||||
|
'available_languages' => $availableLanguages,
|
||||||
'feed_providers' => $feedProviders,
|
'feed_providers' => $feedProviders,
|
||||||
], 'Onboarding options retrieved successfully.');
|
], 'Onboarding options retrieved successfully.');
|
||||||
}
|
}
|
||||||
|
|
@ -352,6 +359,7 @@ public function createRoute(Request $request): JsonResponse
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
'platform_channel_id' => 'required|exists:platform_channels,id',
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
'priority' => 'nullable|integer|min:1|max:100',
|
'priority' => 'nullable|integer|min:1|max:100',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -361,9 +369,22 @@ public function createRoute(Request $request): JsonResponse
|
||||||
|
|
||||||
$validated = $validator->validated();
|
$validated = $validator->validated();
|
||||||
|
|
||||||
|
// Validate language consistency
|
||||||
|
$feed = Feed::find($validated['feed_id']);
|
||||||
|
$channel = PlatformChannel::find($validated['platform_channel_id']);
|
||||||
|
|
||||||
|
if (!$feed || !$feed->languages()->where('languages.id', $validated['language_id'])->exists()) {
|
||||||
|
return $this->sendError('The selected feed does not support the chosen language.', [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$channel || $channel->language_id !== (int)$validated['language_id']) {
|
||||||
|
return $this->sendError('The selected channel does not support the chosen language.', [], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$route = Route::create([
|
$route = Route::create([
|
||||||
'feed_id' => $validated['feed_id'],
|
'feed_id' => $validated['feed_id'],
|
||||||
'platform_channel_id' => $validated['platform_channel_id'],
|
'platform_channel_id' => $validated['platform_channel_id'],
|
||||||
|
'language_id' => $validated['language_id'],
|
||||||
'priority' => $validated['priority'] ?? 50,
|
'priority' => $validated['priority'] ?? 50,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
@ -373,7 +394,7 @@ public function createRoute(Request $request): JsonResponse
|
||||||
ArticleDiscoveryJob::dispatch();
|
ArticleDiscoveryJob::dispatch();
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->load(['feed', 'platformChannel'])),
|
new RouteResource($route->load(['feed', 'platformChannel', 'language'])),
|
||||||
'Route created successfully.'
|
'Route created successfully.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
use Domains\Feed\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Feed\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
|
use Domains\Feed\Requests\StoreRouteRequest;
|
||||||
|
use Domains\Feed\Requests\UpdateRouteRequest;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -17,7 +19,7 @@ class RoutingController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function index(): JsonResponse
|
public function index(): JsonResponse
|
||||||
{
|
{
|
||||||
$routes = Route::with(['feed', 'platformChannel', 'keywords'])
|
$routes = Route::with(['feed', 'platformChannel', 'language', 'keywords'])
|
||||||
->orderBy('is_active', 'desc')
|
->orderBy('is_active', 'desc')
|
||||||
->orderBy('priority', 'asc')
|
->orderBy('priority', 'asc')
|
||||||
->get();
|
->get();
|
||||||
|
|
@ -31,15 +33,10 @@ public function index(): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Store a newly created routing configuration
|
* Store a newly created routing configuration
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(StoreRouteRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$validated = $request->validate([
|
$validated = $request->validated();
|
||||||
'feed_id' => 'required|exists:feeds,id',
|
|
||||||
'platform_channel_id' => 'required|exists:platform_channels,id',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'nullable|integer|min:0',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||||
$validated['priority'] = $validated['priority'] ?? 0;
|
$validated['priority'] = $validated['priority'] ?? 0;
|
||||||
|
|
@ -47,7 +44,7 @@ public function store(Request $request): JsonResponse
|
||||||
$route = Route::create($validated);
|
$route = Route::create($validated);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->load(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->load(['feed', 'platformChannel', 'language', 'keywords'])),
|
||||||
'Routing configuration created successfully!',
|
'Routing configuration created successfully!',
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
|
|
@ -69,7 +66,7 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
return $this->sendNotFound('Routing configuration not found.');
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$route->load(['feed', 'platformChannel', 'keywords']);
|
$route->load(['feed', 'platformChannel', 'language', 'keywords']);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route),
|
new RouteResource($route),
|
||||||
|
|
@ -80,7 +77,7 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
/**
|
/**
|
||||||
* Update the specified routing configuration
|
* Update the specified routing configuration
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Feed $feed, PlatformChannel $channel): JsonResponse
|
public function update(UpdateRouteRequest $request, Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$route = $this->findRoute($feed, $channel);
|
$route = $this->findRoute($feed, $channel);
|
||||||
|
|
@ -89,17 +86,14 @@ public function update(Request $request, Feed $feed, PlatformChannel $channel):
|
||||||
return $this->sendNotFound('Routing configuration not found.');
|
return $this->sendNotFound('Routing configuration not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validated();
|
||||||
'is_active' => 'boolean',
|
|
||||||
'priority' => 'nullable|integer|min:0',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Route::where('feed_id', $feed->id)
|
Route::where('feed_id', $feed->id)
|
||||||
->where('platform_channel_id', $channel->id)
|
->where('platform_channel_id', $channel->id)
|
||||||
->update($validated);
|
->update($validated);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
|
||||||
'Routing configuration updated successfully!'
|
'Routing configuration updated successfully!'
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
|
|
@ -154,7 +148,7 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
|
||||||
$status = $newStatus ? 'activated' : 'deactivated';
|
$status = $newStatus ? 'activated' : 'deactivated';
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new RouteResource($route->fresh(['feed', 'platformChannel', 'keywords'])),
|
new RouteResource($route->fresh(['feed', 'platformChannel', 'language', 'keywords'])),
|
||||||
"Routing configuration {$status} successfully!"
|
"Routing configuration {$status} successfully!"
|
||||||
);
|
);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->foreignId('language_id')->nullable()->after('platform_channel_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->index(['language_id', 'is_active']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate existing routes to set language based on feed's primary language and channel language (where they match)
|
||||||
|
DB::statement("
|
||||||
|
UPDATE routes r
|
||||||
|
INNER JOIN platform_channels pc ON r.platform_channel_id = pc.id
|
||||||
|
INNER JOIN feed_languages fl ON r.feed_id = fl.feed_id AND fl.is_primary = 1 AND fl.is_active = 1
|
||||||
|
SET r.language_id = pc.language_id
|
||||||
|
WHERE pc.language_id = fl.language_id
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('routes', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['language_id']);
|
||||||
|
$table->dropIndex(['language_id', 'is_active']);
|
||||||
|
$table->dropColumn('language_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
use App\Http\Controllers\Api\V1\ArticlesController;
|
use App\Http\Controllers\Api\V1\ArticlesController;
|
||||||
use App\Http\Controllers\Api\V1\DashboardController;
|
use App\Http\Controllers\Api\V1\DashboardController;
|
||||||
use App\Http\Controllers\Api\V1\FeedsController;
|
use App\Http\Controllers\Api\V1\FeedsController;
|
||||||
|
use App\Http\Controllers\Api\V1\LanguagesController;
|
||||||
use App\Http\Controllers\Api\V1\LogsController;
|
use App\Http\Controllers\Api\V1\LogsController;
|
||||||
use App\Http\Controllers\Api\V1\OnboardingController;
|
use App\Http\Controllers\Api\V1\OnboardingController;
|
||||||
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
use App\Http\Controllers\Api\V1\PlatformAccountsController;
|
||||||
|
|
@ -101,6 +102,11 @@
|
||||||
Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index');
|
Route::get('/settings', [SettingsController::class, 'index'])->name('api.settings.index');
|
||||||
Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update');
|
Route::put('/settings', [SettingsController::class, 'update'])->name('api.settings.update');
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
Route::get('/languages/available-for-routes', [LanguagesController::class, 'availableForRoutes'])->name('api.languages.available-for-routes');
|
||||||
|
Route::get('/languages/{language}/feeds', [LanguagesController::class, 'feedsByLanguage'])->name('api.languages.feeds');
|
||||||
|
Route::get('/languages/{language}/channels', [LanguagesController::class, 'channelsByLanguage'])->name('api.languages.channels');
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index');
|
Route::get('/logs', [LogsController::class, 'index'])->name('api.logs.index');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use Domains\Article\Models\Keyword;
|
use Domains\Article\Models\Keyword;
|
||||||
use Domains\Feed\Factories\RouteFactory;
|
use Domains\Feed\Factories\RouteFactory;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
@ -14,10 +15,12 @@
|
||||||
/**
|
/**
|
||||||
* @property int $feed_id
|
* @property int $feed_id
|
||||||
* @property int $platform_channel_id
|
* @property int $platform_channel_id
|
||||||
|
* @property int|null $language_id
|
||||||
* @property bool $is_active
|
* @property bool $is_active
|
||||||
* @property int $priority
|
* @property int $priority
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
|
* @property Language|null $language
|
||||||
*/
|
*/
|
||||||
class Route extends Model
|
class Route extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -38,6 +41,7 @@ protected static function newFactory()
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'feed_id',
|
'feed_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
'language_id',
|
||||||
'is_active',
|
'is_active',
|
||||||
'priority'
|
'priority'
|
||||||
];
|
];
|
||||||
|
|
@ -62,6 +66,14 @@ public function platformChannel(): BelongsTo
|
||||||
return $this->belongsTo(PlatformChannel::class);
|
return $this->belongsTo(PlatformChannel::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Language, $this>
|
||||||
|
*/
|
||||||
|
public function language(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Language::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Keyword, $this>
|
* @return HasMany<Keyword, $this>
|
||||||
*/
|
*/
|
||||||
|
|
@ -70,4 +82,51 @@ public function keywords(): HasMany
|
||||||
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
|
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
|
||||||
->where('platform_channel_id', $this->platform_channel_id);
|
->where('platform_channel_id', $this->platform_channel_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the feed has the given language
|
||||||
|
*/
|
||||||
|
public function feedHasLanguage(int $languageId): bool
|
||||||
|
{
|
||||||
|
if (!$this->feed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->feed->languages()->where('languages.id', $languageId)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the platform channel has the given language
|
||||||
|
*/
|
||||||
|
public function channelHasLanguage(int $languageId): bool
|
||||||
|
{
|
||||||
|
return $this->platformChannel && $this->platformChannel->language_id === $languageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the route's language is consistent with feed and channel
|
||||||
|
*/
|
||||||
|
public function hasConsistentLanguage(): bool
|
||||||
|
{
|
||||||
|
if (!$this->language_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->feedHasLanguage($this->language_id) && $this->channelHasLanguage($this->language_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common languages between feed and channel
|
||||||
|
*/
|
||||||
|
public function getCommonLanguages(): array
|
||||||
|
{
|
||||||
|
if (!$this->feed || !$this->platformChannel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedLanguageIds = $this->feed->languages()->pluck('languages.id')->toArray();
|
||||||
|
$channelLanguageId = $this->platformChannel->language_id;
|
||||||
|
|
||||||
|
return in_array($channelLanguageId, $feedLanguageIds) ? [$channelLanguageId] : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
backend/src/Domains/Feed/Requests/StoreRouteRequest.php
Normal file
91
backend/src/Domains/Feed/Requests/StoreRouteRequest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class StoreRouteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'feed_id' => 'required|exists:feeds,id',
|
||||||
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
||||||
|
'language_id' => 'required|exists:languages,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the validator instance.
|
||||||
|
*/
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator) {
|
||||||
|
$this->validateLanguageConsistency($validator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the language is consistent between feed and channel
|
||||||
|
*/
|
||||||
|
protected function validateLanguageConsistency(Validator $validator): void
|
||||||
|
{
|
||||||
|
$feedId = $this->input('feed_id');
|
||||||
|
$channelId = $this->input('platform_channel_id');
|
||||||
|
$languageId = $this->input('language_id');
|
||||||
|
|
||||||
|
if (!$feedId || !$channelId || !$languageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if feed has the selected language
|
||||||
|
$feed = Feed::find($feedId);
|
||||||
|
if (!$feed || !$feed->languages()->where('languages.id', $languageId)->exists()) {
|
||||||
|
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if channel has the selected language
|
||||||
|
$channel = PlatformChannel::find($channelId);
|
||||||
|
if (!$channel || $channel->language_id !== (int)$languageId) {
|
||||||
|
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'feed_id.required' => 'A feed must be selected.',
|
||||||
|
'feed_id.exists' => 'The selected feed is invalid.',
|
||||||
|
'platform_channel_id.required' => 'A platform channel must be selected.',
|
||||||
|
'platform_channel_id.exists' => 'The selected platform channel is invalid.',
|
||||||
|
'language_id.required' => 'A language must be selected.',
|
||||||
|
'language_id.exists' => 'The selected language is invalid.',
|
||||||
|
'priority.min' => 'Priority must be at least 0.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
89
backend/src/Domains/Feed/Requests/UpdateRouteRequest.php
Normal file
89
backend/src/Domains/Feed/Requests/UpdateRouteRequest.php
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Feed\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class UpdateRouteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_id' => 'nullable|exists:languages,id',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'priority' => 'nullable|integer|min:0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the validator instance.
|
||||||
|
*/
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator) {
|
||||||
|
$this->validateLanguageConsistency($validator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the language is consistent between feed and channel
|
||||||
|
*/
|
||||||
|
protected function validateLanguageConsistency(Validator $validator): void
|
||||||
|
{
|
||||||
|
$languageId = $this->input('language_id');
|
||||||
|
|
||||||
|
// If no language_id provided, skip validation (allowing null updates)
|
||||||
|
if (!$languageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get feed and channel from route parameters
|
||||||
|
$feed = $this->route('feed');
|
||||||
|
$channel = $this->route('channel');
|
||||||
|
|
||||||
|
if (!$feed || !$channel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if feed has the selected language
|
||||||
|
if (!$feed->languages()->where('languages.id', $languageId)->exists()) {
|
||||||
|
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if channel has the selected language
|
||||||
|
if ($channel->language_id !== (int)$languageId) {
|
||||||
|
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_id.exists' => 'The selected language is invalid.',
|
||||||
|
'priority.min' => 'Priority must be at least 0.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use Domains\Feed\Resources\FeedResource;
|
use Domains\Feed\Resources\FeedResource;
|
||||||
use Domains\Platform\Resources\PlatformChannelResource;
|
use Domains\Platform\Resources\PlatformChannelResource;
|
||||||
|
use Domains\Settings\Resources\LanguageResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
|
@ -20,12 +21,14 @@ public function toArray(Request $request): array
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'feed_id' => $this->feed_id,
|
'feed_id' => $this->feed_id,
|
||||||
'platform_channel_id' => $this->platform_channel_id,
|
'platform_channel_id' => $this->platform_channel_id,
|
||||||
|
'language_id' => $this->language_id,
|
||||||
'is_active' => $this->is_active,
|
'is_active' => $this->is_active,
|
||||||
'priority' => $this->priority,
|
'priority' => $this->priority,
|
||||||
'created_at' => $this->created_at->toISOString(),
|
'created_at' => $this->created_at->toISOString(),
|
||||||
'updated_at' => $this->updated_at->toISOString(),
|
'updated_at' => $this->updated_at->toISOString(),
|
||||||
'feed' => new FeedResource($this->whenLoaded('feed')),
|
'feed' => new FeedResource($this->whenLoaded('feed')),
|
||||||
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
|
'platform_channel' => new PlatformChannelResource($this->whenLoaded('platformChannel')),
|
||||||
|
'language' => new LanguageResource($this->whenLoaded('language')),
|
||||||
'keywords' => $this->whenLoaded('keywords', function () {
|
'keywords' => $this->whenLoaded('keywords', function () {
|
||||||
return $this->keywords->map(function ($keyword) {
|
return $this->keywords->map(function ($keyword) {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Domains\Settings\Models;
|
namespace Domains\Settings\Models;
|
||||||
|
|
||||||
use Domains\Feed\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Feed\Models\Route;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Platform\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Domains\Settings\Factories\LanguageFactory;
|
use Domains\Settings\Factories\LanguageFactory;
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class Language extends Model
|
class Language extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -59,4 +61,67 @@ public function feeds(): BelongsToMany
|
||||||
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<Route, $this>
|
||||||
|
*/
|
||||||
|
public function routes(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Route::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get languages that have both active feeds and active channels
|
||||||
|
*
|
||||||
|
* @param Builder<Language> $query
|
||||||
|
* @return Builder<Language>
|
||||||
|
*/
|
||||||
|
public function scopeAvailableForRoutes(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true)
|
||||||
|
->whereHas('feeds', function (Builder $feedQuery) {
|
||||||
|
$feedQuery->where('feeds.is_active', true)
|
||||||
|
->where('feed_languages.is_active', true);
|
||||||
|
})
|
||||||
|
->whereHas('platformChannels', function (Builder $channelQuery) {
|
||||||
|
$channelQuery->where('platform_channels.is_active', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get languages with active feeds
|
||||||
|
*
|
||||||
|
* @param Builder<Language> $query
|
||||||
|
* @return Builder<Language>
|
||||||
|
*/
|
||||||
|
public function scopeWithActiveFeeds(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->whereHas('feeds', function (Builder $feedQuery) {
|
||||||
|
$feedQuery->where('feeds.is_active', true)
|
||||||
|
->where('feed_languages.is_active', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to get languages with active channels
|
||||||
|
*
|
||||||
|
* @param Builder<Language> $query
|
||||||
|
* @return Builder<Language>
|
||||||
|
*/
|
||||||
|
public function scopeWithActiveChannels(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->whereHas('platformChannels', function (Builder $channelQuery) {
|
||||||
|
$channelQuery->where('platform_channels.is_active', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this language can be used for routes (has both feeds and channels)
|
||||||
|
*/
|
||||||
|
public function canBeUsedForRoutes(): bool
|
||||||
|
{
|
||||||
|
return $this->is_active &&
|
||||||
|
$this->feeds()->where('feeds.is_active', true)->where('feed_languages.is_active', true)->exists() &&
|
||||||
|
$this->platformChannels()->where('platform_channels.is_active', true)->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
backend/src/Domains/Settings/Resources/LanguageResource.php
Normal file
25
backend/src/Domains/Settings/Resources/LanguageResource.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domains\Settings\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class LanguageResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'short_code' => $this->short_code,
|
||||||
|
'name' => $this->name,
|
||||||
|
'native_name' => $this->native_name,
|
||||||
|
'is_active' => $this->is_active,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Feed\Models\Route;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class RoutingControllerLanguageTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_store_route_requires_language_id(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/routing', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
// 'language_id' is missing
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonValidationErrors(['language_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_route_validates_language_consistency_with_feed(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
// Attach only language2 to feed, but request language1
|
||||||
|
$feed->languages()->attach($language2->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/routing', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonValidationErrors(['language_id']);
|
||||||
|
$this->assertStringContainsString('The selected feed does not support this language',
|
||||||
|
$response->json('errors.language_id.0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_route_validates_language_consistency_with_channel(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
|
||||||
|
|
||||||
|
// Attach language1 to feed, but channel has language2
|
||||||
|
$feed->languages()->attach($language1->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/routing', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonValidationErrors(['language_id']);
|
||||||
|
$this->assertStringContainsString('The selected channel does not support this language',
|
||||||
|
$response->json('errors.language_id.0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_route_succeeds_with_valid_language_consistency(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/routing', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Routing configuration created successfully!',
|
||||||
|
'data' => [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('routes', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_route_validates_language_consistency(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
// Attach both languages to feed
|
||||||
|
$feed->languages()->attach([
|
||||||
|
$language1->id => [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
],
|
||||||
|
$language2->id => [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => false
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create route with language1
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Try to update to language2, which feed supports but channel doesn't
|
||||||
|
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
|
||||||
|
'language_id' => $language2->id,
|
||||||
|
'is_active' => false,
|
||||||
|
'priority' => 75
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonValidationErrors(['language_id']);
|
||||||
|
$this->assertStringContainsString('The selected channel does not support this language',
|
||||||
|
$response->json('errors.language_id.0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_route_succeeds_with_valid_language(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create route
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->putJson("/api/v1/routing/{$feed->id}/{$channel->id}", [
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => false,
|
||||||
|
'priority' => 75
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Routing configuration updated successfully!',
|
||||||
|
'data' => [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => false,
|
||||||
|
'priority' => 75
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('routes', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => false,
|
||||||
|
'priority' => 75
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_includes_language_relationship(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Create route
|
||||||
|
Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/routing');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'language' => [
|
||||||
|
'id' => $language->id,
|
||||||
|
'name' => $language->name,
|
||||||
|
'short_code' => $language->short_code
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_show_includes_language_relationship(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Create route
|
||||||
|
Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/v1/routing/{$feed->id}/{$channel->id}");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'language' => [
|
||||||
|
'id' => $language->id,
|
||||||
|
'name' => $language->name,
|
||||||
|
'short_code' => $language->short_code
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
namespace Tests\Unit\Models;
|
namespace Tests\Unit\Models;
|
||||||
|
|
||||||
use Domains\Feed\Models\Feed;
|
use Domains\Feed\Models\Feed;
|
||||||
use Domains\Settings\Models\Language;
|
use Domains\Feed\Models\Route;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Platform\Models\PlatformInstance;
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -13,335 +13,139 @@ class LanguageTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_fillable_fields(): void
|
public function test_routes_relationship(): void
|
||||||
{
|
|
||||||
$fillableFields = ['short_code', 'name', 'native_name', 'is_active'];
|
|
||||||
$language = new Language();
|
|
||||||
|
|
||||||
$this->assertEquals($fillableFields, $language->getFillable());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_table_name(): void
|
|
||||||
{
|
|
||||||
$language = new Language();
|
|
||||||
|
|
||||||
$this->assertEquals('languages', $language->getTable());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_casts_is_active_to_boolean(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create(['is_active' => '1']);
|
|
||||||
|
|
||||||
$this->assertIsBool($language->is_active);
|
|
||||||
$this->assertTrue($language->is_active);
|
|
||||||
|
|
||||||
$language->update(['is_active' => '0']);
|
|
||||||
$language->refresh();
|
|
||||||
|
|
||||||
$this->assertIsBool($language->is_active);
|
|
||||||
$this->assertFalse($language->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_belongs_to_many_platform_instances_relationship(): void
|
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance1 = PlatformInstance::factory()->create();
|
$feed1 = Feed::factory()->create();
|
||||||
$instance2 = PlatformInstance::factory()->create();
|
$feed2 = Feed::factory()->create();
|
||||||
|
|
||||||
// Attach with required platform_language_id
|
|
||||||
$language->platformInstances()->attach([
|
|
||||||
$instance1->id => ['platform_language_id' => 1],
|
|
||||||
$instance2->id => ['platform_language_id' => 2]
|
|
||||||
]);
|
|
||||||
|
|
||||||
$instances = $language->platformInstances;
|
|
||||||
|
|
||||||
$this->assertCount(2, $instances);
|
|
||||||
$this->assertTrue($instances->contains('id', $instance1->id));
|
|
||||||
$this->assertTrue($instances->contains('id', $instance2->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_has_many_platform_channels_relationship(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
$channel1 = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
$channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
$channel2 = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
// Create channel for different language
|
$route1 = Route::create([
|
||||||
$otherLanguage = Language::factory()->create();
|
'feed_id' => $feed1->id,
|
||||||
PlatformChannel::factory()->create(['language_id' => $otherLanguage->id]);
|
'platform_channel_id' => $channel1->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
$channels = $language->platformChannels;
|
|
||||||
|
|
||||||
$this->assertCount(2, $channels);
|
|
||||||
$this->assertTrue($channels->contains('id', $channel1->id));
|
|
||||||
$this->assertTrue($channels->contains('id', $channel2->id));
|
|
||||||
$this->assertInstanceOf(PlatformChannel::class, $channels->first());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_belongs_to_many_feeds_relationship(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$feed1 = Feed::factory()->create();
|
|
||||||
$feed2 = Feed::factory()->create();
|
|
||||||
|
|
||||||
// Attach feeds to this language
|
|
||||||
$language->feeds()->attach($feed1->id, [
|
|
||||||
'url' => $feed1->url,
|
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'is_primary' => true
|
'priority' => 50
|
||||||
]);
|
|
||||||
$language->feeds()->attach($feed2->id, [
|
|
||||||
'url' => $feed2->url,
|
|
||||||
'is_active' => true,
|
|
||||||
'is_primary' => false
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create feed for different language
|
$route2 = Route::create([
|
||||||
$otherLanguage = Language::factory()->create();
|
'feed_id' => $feed2->id,
|
||||||
$feed3 = Feed::factory()->create();
|
'platform_channel_id' => $channel2->id,
|
||||||
$otherLanguage->feeds()->attach($feed3->id, [
|
'language_id' => $language->id,
|
||||||
'url' => $feed3->url,
|
'is_active' => false,
|
||||||
'is_active' => true,
|
'priority' => 60
|
||||||
'is_primary' => true
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$feeds = $language->feeds;
|
$routes = $language->routes;
|
||||||
|
|
||||||
$this->assertCount(2, $feeds);
|
$this->assertCount(2, $routes);
|
||||||
$this->assertTrue($feeds->contains('id', $feed1->id));
|
$this->assertTrue($routes->contains('id', $route1->id));
|
||||||
$this->assertTrue($feeds->contains('id', $feed2->id));
|
$this->assertTrue($routes->contains('id', $route2->id));
|
||||||
$this->assertFalse($feeds->contains('id', $feed3->id));
|
|
||||||
$this->assertInstanceOf(Feed::class, $feeds->first());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_creation_with_factory(): void
|
public function test_available_for_routes_scope(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
// Create languages
|
||||||
|
$language1 = Language::factory()->create(['is_active' => true]);
|
||||||
|
$language2 = Language::factory()->create(['is_active' => true]);
|
||||||
|
$language3 = Language::factory()->create(['is_active' => false]); // inactive language
|
||||||
|
|
||||||
$this->assertInstanceOf(Language::class, $language);
|
// Create feeds and channels
|
||||||
$this->assertIsString($language->short_code);
|
$feed1 = Feed::factory()->create(['is_active' => true]);
|
||||||
$this->assertIsString($language->name);
|
$feed2 = Feed::factory()->create(['is_active' => false]); // inactive feed
|
||||||
$this->assertTrue($language->is_active);
|
$channel1 = PlatformChannel::factory()->create(['language_id' => $language1->id, 'is_active' => true]);
|
||||||
|
$channel2 = PlatformChannel::factory()->create(['language_id' => $language2->id, 'is_active' => false]); // inactive channel
|
||||||
|
|
||||||
|
// Attach languages to feeds
|
||||||
|
$feed1->languages()->attach($language1->id, ['url' => $feed1->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
$feed2->languages()->attach($language2->id, ['url' => $feed2->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
$feed1->languages()->attach($language3->id, ['url' => $feed1->url, 'is_active' => true, 'is_primary' => false]);
|
||||||
|
|
||||||
|
$availableLanguages = Language::availableForRoutes()->get();
|
||||||
|
|
||||||
|
// Only language1 should be available (has active feed and active channel)
|
||||||
|
$this->assertCount(1, $availableLanguages);
|
||||||
|
$this->assertEquals($language1->id, $availableLanguages->first()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_creation_with_explicit_values(): void
|
public function test_with_active_feeds_scope(): void
|
||||||
{
|
{
|
||||||
$language = Language::create([
|
$language1 = Language::factory()->create(['is_active' => true]);
|
||||||
'short_code' => 'fr',
|
$language2 = Language::factory()->create(['is_active' => true]);
|
||||||
'name' => 'French',
|
|
||||||
'native_name' => 'Français',
|
|
||||||
'is_active' => false
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertEquals('fr', $language->short_code);
|
$activeFeed = Feed::factory()->create(['is_active' => true]);
|
||||||
$this->assertEquals('French', $language->name);
|
$inactiveFeed = Feed::factory()->create(['is_active' => false]);
|
||||||
$this->assertEquals('Français', $language->native_name);
|
|
||||||
$this->assertFalse($language->is_active);
|
// Attach languages to feeds
|
||||||
|
$activeFeed->languages()->attach($language1->id, ['url' => $activeFeed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
$inactiveFeed->languages()->attach($language2->id, ['url' => $inactiveFeed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
|
||||||
|
$languagesWithActiveFeeds = Language::withActiveFeeds()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $languagesWithActiveFeeds);
|
||||||
|
$this->assertEquals($language1->id, $languagesWithActiveFeeds->first()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_factory_states(): void
|
public function test_with_active_channels_scope(): void
|
||||||
{
|
{
|
||||||
$inactiveLanguage = Language::factory()->inactive()->create();
|
$language1 = Language::factory()->create(['is_active' => true]);
|
||||||
$this->assertFalse($inactiveLanguage->is_active);
|
$language2 = Language::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
$englishLanguage = Language::factory()->english()->create();
|
PlatformChannel::factory()->create(['language_id' => $language1->id, 'is_active' => true]);
|
||||||
$this->assertEquals('en', $englishLanguage->short_code);
|
PlatformChannel::factory()->create(['language_id' => $language2->id, 'is_active' => false]);
|
||||||
$this->assertEquals('English', $englishLanguage->name);
|
|
||||||
$this->assertEquals('English', $englishLanguage->native_name);
|
$languagesWithActiveChannels = Language::withActiveChannels()->get();
|
||||||
|
|
||||||
|
$this->assertCount(1, $languagesWithActiveChannels);
|
||||||
|
$this->assertEquals($language1->id, $languagesWithActiveChannels->first()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_update(): void
|
public function test_can_be_used_for_routes_method(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create([
|
$language = Language::factory()->create(['is_active' => true]);
|
||||||
'name' => 'Original Name',
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
'is_active' => true
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
|
||||||
]);
|
|
||||||
|
|
||||||
$language->update([
|
// Attach language to feed
|
||||||
'name' => 'Updated Name',
|
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
'is_active' => false
|
|
||||||
]);
|
|
||||||
|
|
||||||
$language->refresh();
|
$this->assertTrue($language->canBeUsedForRoutes());
|
||||||
|
|
||||||
$this->assertEquals('Updated Name', $language->name);
|
|
||||||
$this->assertFalse($language->is_active);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_deletion(): void
|
public function test_can_be_used_for_routes_method_returns_false_when_language_inactive(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create(['is_active' => false]);
|
||||||
$languageId = $language->id;
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
|
||||||
|
|
||||||
$language->delete();
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
|
||||||
$this->assertDatabaseMissing('languages', ['id' => $languageId]);
|
$this->assertFalse($language->canBeUsedForRoutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_can_have_null_native_name(): void
|
public function test_can_be_used_for_routes_method_returns_false_when_no_active_feeds(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create(['native_name' => null]);
|
$language = Language::factory()->create(['is_active' => true]);
|
||||||
|
$feed = Feed::factory()->create(['is_active' => false]); // inactive feed
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => true]);
|
||||||
|
|
||||||
$this->assertNull($language->native_name);
|
// Attach language to inactive feed
|
||||||
|
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
|
||||||
|
$this->assertFalse($language->canBeUsedForRoutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_can_have_empty_native_name(): void
|
public function test_can_be_used_for_routes_method_returns_false_when_no_active_channels(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create(['native_name' => '']);
|
$language = Language::factory()->create(['is_active' => true]);
|
||||||
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id, 'is_active' => false]); // inactive channel
|
||||||
|
|
||||||
$this->assertEquals('', $language->native_name);
|
// Attach language to feed
|
||||||
}
|
$feed->languages()->attach($language->id, ['url' => $feed->url, 'is_active' => true, 'is_primary' => true]);
|
||||||
|
|
||||||
public function test_language_short_code_variations(): void
|
$this->assertFalse($language->canBeUsedForRoutes());
|
||||||
{
|
|
||||||
$shortCodes = ['en', 'fr', 'es', 'de', 'zh', 'pt', 'nl', 'it'];
|
|
||||||
|
|
||||||
foreach ($shortCodes as $code) {
|
|
||||||
$language = Language::factory()->create(['short_code' => $code]);
|
|
||||||
$this->assertEquals($code, $language->short_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_timestamps(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$this->assertNotNull($language->created_at);
|
|
||||||
$this->assertNotNull($language->updated_at);
|
|
||||||
$this->assertInstanceOf(\Carbon\Carbon::class, $language->created_at);
|
|
||||||
$this->assertInstanceOf(\Carbon\Carbon::class, $language->updated_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_can_have_multiple_platform_instances(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
$instance1 = PlatformInstance::factory()->create();
|
|
||||||
$instance2 = PlatformInstance::factory()->create();
|
|
||||||
$instance3 = PlatformInstance::factory()->create();
|
|
||||||
|
|
||||||
// Attach with required platform_language_id values
|
|
||||||
$language->platformInstances()->attach([
|
|
||||||
$instance1->id => ['platform_language_id' => 1],
|
|
||||||
$instance2->id => ['platform_language_id' => 2],
|
|
||||||
$instance3->id => ['platform_language_id' => 3]
|
|
||||||
]);
|
|
||||||
|
|
||||||
$instances = $language->platformInstances;
|
|
||||||
|
|
||||||
$this->assertCount(3, $instances);
|
|
||||||
$this->assertTrue($instances->contains('id', $instance1->id));
|
|
||||||
$this->assertTrue($instances->contains('id', $instance2->id));
|
|
||||||
$this->assertTrue($instances->contains('id', $instance3->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_platform_instances_relationship_is_empty_by_default(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$this->assertCount(0, $language->platformInstances);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_platform_channels_relationship_is_empty_by_default(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$this->assertCount(0, $language->platformChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_feeds_relationship_is_empty_by_default(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
$this->assertCount(0, $language->feeds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_multiple_languages_with_same_name_different_regions(): void
|
|
||||||
{
|
|
||||||
$englishUS = Language::factory()->create([
|
|
||||||
'short_code' => 'en-US',
|
|
||||||
'name' => 'English (United States)',
|
|
||||||
'native_name' => 'English'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$englishGB = Language::factory()->create([
|
|
||||||
'short_code' => 'en-GB',
|
|
||||||
'name' => 'English (United Kingdom)',
|
|
||||||
'native_name' => 'English'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertEquals('English', $englishUS->native_name);
|
|
||||||
$this->assertEquals('English', $englishGB->native_name);
|
|
||||||
$this->assertNotEquals($englishUS->short_code, $englishGB->short_code);
|
|
||||||
$this->assertNotEquals($englishUS->name, $englishGB->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_with_complex_native_name(): void
|
|
||||||
{
|
|
||||||
$complexLanguages = [
|
|
||||||
['short_code' => 'zh-CN', 'name' => 'Chinese (Simplified)', 'native_name' => '简体中文'],
|
|
||||||
['short_code' => 'zh-TW', 'name' => 'Chinese (Traditional)', 'native_name' => '繁體中文'],
|
|
||||||
['short_code' => 'ar', 'name' => 'Arabic', 'native_name' => 'العربية'],
|
|
||||||
['short_code' => 'ru', 'name' => 'Russian', 'native_name' => 'Русский'],
|
|
||||||
['short_code' => 'ja', 'name' => 'Japanese', 'native_name' => '日本語'],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($complexLanguages as $langData) {
|
|
||||||
$language = Language::factory()->create($langData);
|
|
||||||
|
|
||||||
$this->assertEquals($langData['short_code'], $language->short_code);
|
|
||||||
$this->assertEquals($langData['name'], $language->name);
|
|
||||||
$this->assertEquals($langData['native_name'], $language->native_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_active_and_inactive_states(): void
|
|
||||||
{
|
|
||||||
$activeLanguage = Language::factory()->create(['is_active' => true]);
|
|
||||||
$inactiveLanguage = Language::factory()->create(['is_active' => false]);
|
|
||||||
|
|
||||||
$this->assertTrue($activeLanguage->is_active);
|
|
||||||
$this->assertFalse($inactiveLanguage->is_active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_relationships_maintain_referential_integrity(): void
|
|
||||||
{
|
|
||||||
$language = Language::factory()->create();
|
|
||||||
|
|
||||||
// Create related models
|
|
||||||
$instance = PlatformInstance::factory()->create();
|
|
||||||
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
|
||||||
$feed = Feed::factory()->create();
|
|
||||||
$language->feeds()->attach($feed->id, [
|
|
||||||
'url' => $feed->url,
|
|
||||||
'is_active' => true,
|
|
||||||
'is_primary' => true
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Attach instance
|
|
||||||
$language->platformInstances()->attach($instance->id, [
|
|
||||||
'platform_language_id' => 1,
|
|
||||||
'is_default' => true
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Verify all relationships work
|
|
||||||
$this->assertCount(1, $language->platformInstances);
|
|
||||||
$this->assertCount(1, $language->platformChannels);
|
|
||||||
$this->assertCount(1, $language->feeds);
|
|
||||||
|
|
||||||
$this->assertEquals($language->id, $channel->language_id);
|
|
||||||
$this->assertTrue($language->feeds->contains('id', $feed->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_language_factory_unique_constraints(): void
|
|
||||||
{
|
|
||||||
// The factory should generate unique short codes
|
|
||||||
$language1 = Language::factory()->create();
|
|
||||||
$language2 = Language::factory()->create();
|
|
||||||
|
|
||||||
$this->assertNotEquals($language1->short_code, $language2->short_code);
|
|
||||||
$this->assertNotEquals($language1->name, $language2->name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use Domains\Article\Models\Keyword;
|
use Domains\Article\Models\Keyword;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Feed\Models\Route;
|
use Domains\Feed\Models\Route;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ class RouteTest extends TestCase
|
||||||
|
|
||||||
public function test_fillable_fields(): void
|
public function test_fillable_fields(): void
|
||||||
{
|
{
|
||||||
$fillableFields = ['feed_id', 'platform_channel_id', 'is_active', 'priority'];
|
$fillableFields = ['feed_id', 'platform_channel_id', 'language_id', 'is_active', 'priority'];
|
||||||
$route = new Route();
|
$route = new Route();
|
||||||
|
|
||||||
$this->assertEquals($fillableFields, $route->getFillable());
|
$this->assertEquals($fillableFields, $route->getFillable());
|
||||||
|
|
@ -258,4 +259,218 @@ public function test_route_with_multiple_keywords_active_and_inactive(): void
|
||||||
$this->assertEquals('active_keyword', $activeKeywords->first()->keyword);
|
$this->assertEquals('active_keyword', $activeKeywords->first()->keyword);
|
||||||
$this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword);
|
$this->assertEquals('inactive_keyword', $inactiveKeywords->first()->keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_belongs_to_language_relationship(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Language::class, $route->language);
|
||||||
|
$this->assertEquals($language->id, $route->language->id);
|
||||||
|
$this->assertEquals($language->name, $route->language->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_route_creation_with_language(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 75
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($language->id, $route->language_id);
|
||||||
|
$this->assertEquals($feed->id, $route->feed_id);
|
||||||
|
$this->assertEquals($channel->id, $route->platform_channel_id);
|
||||||
|
$this->assertTrue($route->is_active);
|
||||||
|
$this->assertEquals(75, $route->priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feed_has_language_method(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
// Attach only language1 to feed
|
||||||
|
$feed->languages()->attach($language1->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load('feed');
|
||||||
|
|
||||||
|
$this->assertTrue($route->feedHasLanguage($language1->id));
|
||||||
|
$this->assertFalse($route->feedHasLanguage($language2->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channel_has_language_method(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load('platformChannel');
|
||||||
|
|
||||||
|
$this->assertTrue($route->channelHasLanguage($language1->id));
|
||||||
|
$this->assertFalse($route->channelHasLanguage($language2->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_has_consistent_language_method(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load(['feed', 'platformChannel']);
|
||||||
|
|
||||||
|
$this->assertTrue($route->hasConsistentLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_has_consistent_language_method_returns_false_for_mismatched_languages(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
|
||||||
|
|
||||||
|
// Attach only language1 to feed, but channel has language2
|
||||||
|
$feed->languages()->attach($language1->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load(['feed', 'platformChannel']);
|
||||||
|
|
||||||
|
$this->assertFalse($route->hasConsistentLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_common_languages_method(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
// Attach both languages to feed
|
||||||
|
$feed->languages()->attach([
|
||||||
|
$language1->id => [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
],
|
||||||
|
$language2->id => [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => false
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load(['feed', 'platformChannel']);
|
||||||
|
|
||||||
|
$commonLanguages = $route->getCommonLanguages();
|
||||||
|
|
||||||
|
$this->assertEquals([$language1->id], $commonLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_common_languages_method_returns_empty_for_no_match(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
|
||||||
|
|
||||||
|
// Attach only language1 to feed, but channel has language2
|
||||||
|
$feed->languages()->attach($language1->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = Route::create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route->load(['feed', 'platformChannel']);
|
||||||
|
|
||||||
|
$commonLanguages = $route->getCommonLanguages();
|
||||||
|
|
||||||
|
$this->assertEmpty($commonLanguages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
189
backend/tests/Unit/Requests/StoreRouteRequestTest.php
Normal file
189
backend/tests/Unit/Requests/StoreRouteRequestTest.php
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Requests;
|
||||||
|
|
||||||
|
use Domains\Feed\Models\Feed;
|
||||||
|
use Domains\Feed\Requests\StoreRouteRequest;
|
||||||
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
|
use Domains\Settings\Models\Language;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class StoreRouteRequestTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_authorize_returns_true(): void
|
||||||
|
{
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
|
||||||
|
$this->assertTrue($request->authorize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_rules_include_required_fields(): void
|
||||||
|
{
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$rules = $request->rules();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('feed_id', $rules);
|
||||||
|
$this->assertArrayHasKey('platform_channel_id', $rules);
|
||||||
|
$this->assertArrayHasKey('language_id', $rules);
|
||||||
|
$this->assertArrayHasKey('is_active', $rules);
|
||||||
|
$this->assertArrayHasKey('priority', $rules);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('required', $rules['feed_id']);
|
||||||
|
$this->assertStringContainsString('exists:feeds,id', $rules['feed_id']);
|
||||||
|
$this->assertStringContainsString('required', $rules['platform_channel_id']);
|
||||||
|
$this->assertStringContainsString('exists:platform_channels,id', $rules['platform_channel_id']);
|
||||||
|
$this->assertStringContainsString('required', $rules['language_id']);
|
||||||
|
$this->assertStringContainsString('exists:languages,id', $rules['language_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validation_passes_with_valid_data(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$rules = $request->rules();
|
||||||
|
$validator = Validator::make($data, $rules);
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validation_fails_without_required_fields(): void
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$rules = $request->rules();
|
||||||
|
$validator = Validator::make($data, $rules);
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes());
|
||||||
|
$this->assertArrayHasKey('feed_id', $validator->errors()->toArray());
|
||||||
|
$this->assertArrayHasKey('platform_channel_id', $validator->errors()->toArray());
|
||||||
|
$this->assertArrayHasKey('language_id', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validation_fails_with_invalid_foreign_keys(): void
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'feed_id' => 999,
|
||||||
|
'platform_channel_id' => 999,
|
||||||
|
'language_id' => 999,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$rules = $request->rules();
|
||||||
|
$validator = Validator::make($data, $rules);
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes());
|
||||||
|
$this->assertArrayHasKey('feed_id', $validator->errors()->toArray());
|
||||||
|
$this->assertArrayHasKey('platform_channel_id', $validator->errors()->toArray());
|
||||||
|
$this->assertArrayHasKey('language_id', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_language_consistency_validation_fails_when_feed_lacks_language(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language1->id]);
|
||||||
|
|
||||||
|
// Attach only language2 to feed, but request language1
|
||||||
|
$feed->languages()->attach($language2->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$request->merge($data);
|
||||||
|
|
||||||
|
$rules = $request->rules();
|
||||||
|
$validator = Validator::make($data, $rules);
|
||||||
|
|
||||||
|
// Call the custom validation method
|
||||||
|
$request->withValidator($validator);
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes());
|
||||||
|
$this->assertStringContainsString('The selected feed does not support this language',
|
||||||
|
$validator->errors()->first('language_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_language_consistency_validation_fails_when_channel_lacks_language(): void
|
||||||
|
{
|
||||||
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create(['language_id' => $language2->id]);
|
||||||
|
|
||||||
|
// Attach language1 to feed, but channel has language2
|
||||||
|
$feed->languages()->attach($language1->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'language_id' => $language1->id,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 50
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$request->merge($data);
|
||||||
|
|
||||||
|
$rules = $request->rules();
|
||||||
|
$validator = Validator::make($data, $rules);
|
||||||
|
|
||||||
|
// Call the custom validation method
|
||||||
|
$request->withValidator($validator);
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes());
|
||||||
|
$this->assertStringContainsString('The selected channel does not support this language',
|
||||||
|
$validator->errors()->first('language_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_custom_messages(): void
|
||||||
|
{
|
||||||
|
$request = new StoreRouteRequest();
|
||||||
|
$messages = $request->messages();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('feed_id.required', $messages);
|
||||||
|
$this->assertArrayHasKey('platform_channel_id.required', $messages);
|
||||||
|
$this->assertArrayHasKey('language_id.required', $messages);
|
||||||
|
$this->assertEquals('A feed must be selected.', $messages['feed_id.required']);
|
||||||
|
$this->assertEquals('A platform channel must be selected.', $messages['platform_channel_id.required']);
|
||||||
|
$this->assertEquals('A language must be selected.', $messages['language_id.required']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue