Files
daily-meal-api/database/seeders/DemoContentSeeder.php
T
leonm 238a044d98
CI / 🧪 Tests Laravel (push) Successful in 2m33s
CI / 🐳 Build & Push Images (push) Successful in 49s
feat: seeder
2026-08-14 13:43:35 +02:00

508 lines
22 KiB
PHP
Raw 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
namespace Database\Seeders;
use App\Enums\DietType;
use App\Enums\FollowStatus;
use App\Enums\IngredientUnit;
use App\Enums\MealPostType;
use App\Enums\MealPostVisibility;
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use App\Enums\WorkoutSource;
use App\Models\User;
use App\Models\WeightEntry;
use App\Models\WorkoutSessions;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use LogicException;
class DemoContentSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Run the database seeds.
*/
public function run(): void
{
DB::transaction(function (): void {
$users = $this->createUsers();
$this->createMeals($users);
$this->createSocialGraph($users);
$this->createProgressHistory($this->demoUser($users));
});
}
/**
* @return Collection<int, User>
*/
private function createUsers(): Collection
{
$users = collect([
[
'name' => 'Alex Martin',
'email' => 'test@example.com',
'locale' => 'fr',
'bio' => 'Cuisine simple, course à pied et progression régulière.',
'account_verified_at' => now(),
'height' => 178,
'target_weight' => 68,
'weight_goal' => WeightGoal::LOSE_WEIGHT,
'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE,
'pace_preference' => PacePreference::NORMAL,
'sex' => UserSex::MAN,
'date_of_birth' => '1994-04-16',
'terms_accepted_at' => now(),
],
[
'name' => 'Camille Green',
'email' => 'camille@bowli.test',
'locale' => 'fr',
'bio' => 'Recettes végétales colorées et faciles à préparer.',
'account_verified_at' => now(),
],
[
'name' => 'Noah Kitchen',
'email' => 'noah@bowli.test',
'locale' => 'en',
'bio' => 'High-protein meals without complicated preparation.',
],
[
'name' => 'Lina Fraîcheur',
'email' => 'lina@bowli.test',
'locale' => 'fr',
'bio' => 'Bols frais, produits de saison et cuisine méditerranéenne.',
'account_verified_at' => now(),
],
[
'name' => 'Sam Fit Food',
'email' => 'sam@bowli.test',
'locale' => 'en',
'bio' => 'Balanced meals for active weeks.',
],
[
'name' => 'Inès Brunch',
'email' => 'ines@bowli.test',
'locale' => 'fr',
'bio' => 'Petits-déjeuners gourmands et brunchs du dimanche.',
],
[
'name' => 'Hugo Maison',
'email' => 'hugo@bowli.test',
'locale' => 'fr',
'bio' => 'Cuisine maison généreuse avec des ingrédients simples.',
],
[
'name' => 'Maya Plant',
'email' => 'maya@bowli.test',
'locale' => 'en',
'bio' => 'Plant-based bowls, soups and snacks.',
'account_verified_at' => now(),
],
[
'name' => 'Louis Endurance',
'email' => 'louis@bowli.test',
'locale' => 'fr',
'bio' => 'Des repas adaptés aux longues sorties et à la récupération.',
],
])->map(
fn (array $profile): User => User::factory()->create($profile)
);
User::factory()->withoutNutritionOnboarding()->create([
'name' => 'Nouveau profil',
'email' => 'onboarding@example.com',
'locale' => 'fr',
'terms_accepted_at' => now(),
]);
return $users;
}
/**
* @param Collection<int, User> $users
*/
private function createMeals(Collection $users): void
{
$templates = collect($this->mealTemplates());
$reviewComments = collect([
'Très bonne idée, je vais essayer cette semaine.',
'Simple, équilibré et vraiment appétissant.',
'Les proportions sont parfaites pour mon déjeuner.',
'Testé hier soir, toute la famille a aimé.',
'Belle assiette et excellente source dinspiration.',
'Parfait après une séance de sport.',
]);
foreach (range(0, 47) as $index) {
$owner = $users->get($index % $users->count());
$template = $templates->get($index % $templates->count());
$eatenAt = now()->subHours(($index * 4) + 1);
if (! $owner instanceof User || ! is_array($template)) {
throw new LogicException('Unable to build the demo meal collection.');
}
$meal = $owner->mealPosts()->create([
'image_url' => $template['imageUrl'],
'caption' => $template['caption'],
'calories' => $template['calories'],
'proteins' => $template['proteins'],
'carbs' => $template['carbs'],
'fats' => $template['fats'],
'title' => $template['title'],
'eaten_at' => $eatenAt,
'visibility' => MealPostVisibility::Public,
'type' => $template['type'],
'ai_generated' => $index % 9 === 0,
'diet_type' => $template['dietType'],
]);
$meal->ingredients()->createMany(
collect($template['ingredients'])
->values()
->map(fn (array $ingredient, int $position): array => [
...$ingredient,
'position' => $position + 1,
])
->all()
);
$eligibleUsers = $users
->reject(fn (User $user): bool => $user->is($owner))
->values();
$engagedUsers = $eligibleUsers
->slice($index % $eligibleUsers->count())
->concat($eligibleUsers->take($index % $eligibleUsers->count()))
->values();
$meal->likedByUsers()->attach(
$engagedUsers->take(2 + ($index % 5))->pluck('id')->all(),
[
'created_at' => $eatenAt->addMinutes(30),
'updated_at' => $eatenAt->addMinutes(30),
]
);
$engagedUsers->take(2 + ($index % 2))->each(
function (User $reviewer, int $reviewerIndex) use ($index, $meal, $reviewComments): void {
$meal->reviews()->create([
'rating' => 5 - (($index + $reviewerIndex) % 2),
'comment' => $reviewComments->get(
($index + $reviewerIndex) % $reviewComments->count()
),
'user_id' => $reviewer->getKey(),
]);
}
);
}
}
/**
* @param Collection<int, User> $users
*/
private function createSocialGraph(Collection $users): void
{
$demoUser = $this->demoUser($users);
$creators = $users
->reject(fn (User $user): bool => $user->is($demoUser))
->values();
$pivot = [
'status' => FollowStatus::Accepted->value,
'created_at' => now()->subDays(10),
'updated_at' => now()->subDays(10),
];
$demoUser->following()->attach($creators->pluck('id')->all(), $pivot);
$creators->take(5)->each(
fn (User $creator) => $creator->following()->attach($demoUser->getKey(), $pivot)
);
$creators->each(function (User $creator, int $index) use ($creators, $pivot): void {
$nextCreator = $creators->get(($index + 1) % $creators->count());
if ($nextCreator instanceof User) {
$creator->following()->attach($nextCreator->getKey(), $pivot);
}
});
}
private function createProgressHistory(User $demoUser): void
{
$demoUser->weightEntries()->delete();
foreach (range(11, 0) as $weeksAgo) {
WeightEntry::factory()->for($demoUser)->create([
'weight_kg' => round(74.8 - ((11 - $weeksAgo) * 0.28), 2),
'measured_at' => now()->startOfDay()->subWeeks($weeksAgo),
]);
}
$demoUser->nutritionPlans()
->where('is_active', true)
->update([
'calories' => 2050,
'proteins' => 130,
'carbs' => 235,
'fats' => 65,
'maintenance_calories' => 2350,
'calorie_adjustment_percent' => -0.1277,
'calculation_details' => [
'activityFactor' => 1.55,
'inputWeightKg' => 71.72,
],
]);
$workouts = collect([
['title' => 'Course facile', 'type' => 'running', 'duration' => 2700, 'calories' => 390, 'distance' => 7200],
['title' => 'Renforcement complet', 'type' => 'strength', 'duration' => 3300, 'calories' => 310, 'distance' => 0],
['title' => 'Sortie vélo', 'type' => 'cycling', 'duration' => 5400, 'calories' => 620, 'distance' => 32000],
['title' => 'Marche active', 'type' => 'walking', 'duration' => 3600, 'calories' => 260, 'distance' => 6100],
]);
foreach (range(0, 11) as $index) {
$workout = $workouts->get($index % $workouts->count());
$source = $index % 4 === 0 ? WorkoutSource::STRAVA : WorkoutSource::MANUAL;
if (! is_array($workout)) {
throw new LogicException('Unable to build the demo workout collection.');
}
WorkoutSessions::factory()->for($demoUser)->create([
'external_id' => $source === WorkoutSource::STRAVA ? "demo-strava-{$index}" : null,
'source' => $source,
'type' => $workout['type'],
'title' => $workout['title'],
'duration_seconds' => $workout['duration'],
'calories_burned' => $workout['calories'],
'distance_meters' => $workout['distance'],
'started_at' => now()->subDays($index)->setTime(18, 30),
]);
}
}
/**
* @param Collection<int, User> $users
*/
private function demoUser(Collection $users): User
{
$demoUser = $users->first(
fn (User $user): bool => $user->email === 'test@example.com'
);
if (! $demoUser instanceof User) {
throw new LogicException('The demo user could not be created.');
}
return $demoUser;
}
/**
* @return list<array<string, mixed>>
*/
private function mealTemplates(): array
{
return [
[
'title' => 'Bowl saumon et avocat',
'caption' => 'Un déjeuner complet avec du riz, des légumes croquants et une sauce citronnée.',
'imageUrl' => 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&w=1200&q=85',
'calories' => 620,
'proteins' => 38,
'carbs' => 58,
'fats' => 24,
'type' => MealPostType::LUNCH,
'dietType' => DietType::PESCATARIAN,
'ingredients' => [
['ingredient' => 'Saumon', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Riz complet', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Pancakes aux fruits rouges',
'caption' => 'Des pancakes moelleux accompagnés de fruits rouges et de yaourt.',
'imageUrl' => 'https://images.unsplash.com/photo-1528207776546-365bb710ee93?auto=format&fit=crop&w=1200&q=85',
'calories' => 480,
'proteins' => 24,
'carbs' => 64,
'fats' => 14,
'type' => MealPostType::BREAKFAST,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Flocons davoine', 'quantity' => 80, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Fruits rouges', 'quantity' => 120, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Poulet rôti et légumes',
'caption' => 'Une assiette familiale, riche en protéines et pleine de couleurs.',
'imageUrl' => 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?auto=format&fit=crop&w=1200&q=85',
'calories' => 590,
'proteins' => 52,
'carbs' => 46,
'fats' => 19,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Blanc de poulet', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pommes de terre', 'quantity' => 220, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Légumes rôtis', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Curry de pois chiches',
'caption' => 'Un curry végétal doux, crémeux et parfait pour le batch cooking.',
'imageUrl' => 'https://images.unsplash.com/photo-1603894584373-5ac82b2ae398?auto=format&fit=crop&w=1200&q=85',
'calories' => 540,
'proteins' => 21,
'carbs' => 72,
'fats' => 18,
'type' => MealPostType::DINNER,
'dietType' => DietType::VEGAN,
'ingredients' => [
['ingredient' => 'Pois chiches', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Lait de coco', 'quantity' => 120, 'unit' => IngredientUnit::MILLILITER],
['ingredient' => 'Épinards', 'quantity' => 100, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Pâtes au pesto maison',
'caption' => 'Un classique rapide avec tomates cerises, roquette et parmesan.',
'imageUrl' => 'https://images.unsplash.com/photo-1473093295043-cdd812d0e601?auto=format&fit=crop&w=1200&q=85',
'calories' => 670,
'proteins' => 25,
'carbs' => 88,
'fats' => 24,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Pâtes complètes', 'quantity' => 190, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pesto', 'quantity' => 2, 'unit' => IngredientUnit::TABLESPOON],
['ingredient' => 'Tomates cerises', 'quantity' => 120, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Toast avocat et œufs',
'caption' => 'Le brunch simple qui tient toute la matinée.',
'imageUrl' => 'https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1200&q=85',
'calories' => 510,
'proteins' => 26,
'carbs' => 42,
'fats' => 27,
'type' => MealPostType::BRUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Pain complet', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Tacos de poisson',
'caption' => 'Poisson grillé, chou croquant et sauce au yaourt citronnée.',
'imageUrl' => 'https://images.unsplash.com/photo-1551504734-5ee1c4a1479b?auto=format&fit=crop&w=1200&q=85',
'calories' => 560,
'proteins' => 36,
'carbs' => 61,
'fats' => 20,
'type' => MealPostType::DINNER,
'dietType' => DietType::PESCATARIAN,
'ingredients' => [
['ingredient' => 'Poisson blanc', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Tortillas', 'quantity' => 3, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Chou rouge', 'quantity' => 100, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Buddha bowl au tofu',
'caption' => 'Tofu croustillant, quinoa, légumes et sauce sésame.',
'imageUrl' => 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=1200&q=85',
'calories' => 575,
'proteins' => 29,
'carbs' => 69,
'fats' => 22,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGAN,
'ingredients' => [
['ingredient' => 'Tofu', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Quinoa', 'quantity' => 150, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Brocoli', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Steak et patate douce',
'caption' => 'Une assiette protéinée avec patate douce rôtie et haricots verts.',
'imageUrl' => 'https://images.unsplash.com/photo-1600891964092-4316c288032e?auto=format&fit=crop&w=1200&q=85',
'calories' => 710,
'proteins' => 49,
'carbs' => 65,
'fats' => 28,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Steak de bœuf', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Patate douce', 'quantity' => 240, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Haricots verts', 'quantity' => 150, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Overnight oats banane',
'caption' => 'Préparé la veille avec banane, graines de chia et beurre de cacahuète.',
'imageUrl' => 'https://images.unsplash.com/photo-1517673400267-0251440c45dc?auto=format&fit=crop&w=1200&q=85',
'calories' => 445,
'proteins' => 18,
'carbs' => 62,
'fats' => 15,
'type' => MealPostType::BREAKFAST,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Flocons davoine', 'quantity' => 70, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Banane', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Graines de chia', 'quantity' => 2, 'unit' => IngredientUnit::TEASPOON],
],
],
[
'title' => 'Salade grecque complète',
'caption' => 'Tomates, concombre, feta, olives et pain pita.',
'imageUrl' => 'https://images.unsplash.com/photo-1540420773420-3366772f4999?auto=format&fit=crop&w=1200&q=85',
'calories' => 495,
'proteins' => 19,
'carbs' => 47,
'fats' => 26,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Tomates', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Feta', 'quantity' => 80, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pain pita', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Ramen au poulet',
'caption' => 'Un bouillon chaud avec nouilles, poulet, œuf et légumes.',
'imageUrl' => 'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=1200&q=85',
'calories' => 640,
'proteins' => 42,
'carbs' => 78,
'fats' => 18,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Nouilles', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Poulet', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Œuf', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
];
}
}