refactor: Use ActivityService to create system messages in seeder

This commit is contained in:
Daan Meijer 2026-06-17 01:13:45 +02:00
parent 3c414e1ef1
commit 93e8f035e6
10 changed files with 453 additions and 200 deletions

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Models\Dynamic;
use App\Models\PredefinedMutation;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PredefinedMutationController extends Controller
{
use AuthorizesRequests;
/**
* Display a listing of the resource.
*/
public function index(Dynamic $dynamic)
{
$this->authorize('update', $dynamic);
return Inertia::render('Dynamics/PredefinedMutations/Index', [
'dynamic' => $dynamic,
'predefined_mutations' => $dynamic->predefinedMutations()->latest()->get(),
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, Dynamic $dynamic)
{
$this->authorize('update', $dynamic);
$request->validate([
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'amount' => ['required', 'integer'],
'type' => ['required', 'string', 'in:reward,penalty'],
]);
$dynamic->predefinedMutations()->create($request->all());
return redirect()->route('dynamics.predefined-mutations.index', $dynamic);
}
}

View File

@ -20,6 +20,7 @@ class Mutation extends Model
'amount',
'description',
'status',
'predefined_mutation_id',
];
public function ledger(): BelongsTo
@ -32,6 +33,11 @@ class Mutation extends Model
return $this->belongsTo(User::class);
}
public function predefinedMutation(): BelongsTo
{
return $this->belongsTo(PredefinedMutation::class);
}
public function chat(): MorphOne
{
return $this->morphOne(Chat::class, 'chatable');

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PredefinedMutation extends Model
{
use HasFactory;
protected $fillable = [
'dynamic_id',
'name',
'description',
'amount',
'type',
];
public function dynamic(): BelongsTo
{
return $this->belongsTo(Dynamic::class);
}
}

View File

@ -45,6 +45,31 @@ class ActivityService
return $cursor ? $cursor->read_at : Carbon::parse('1970-01-01');
}
public function createMessage($chat, $user, $content, $subject = null)
{
$message = $chat->messages()->create([
'user_id' => $user ? $user->id : null,
'content' => $content,
'subject_id' => $subject ? $subject->id : null,
'subject_type' => $subject ? get_class($subject) : null,
]);
return $message;
}
public function createMutation($ledger, $user, $type, $amount, $description, $status)
{
$mutation = $ledger->mutations()->create([
'user_id' => $user->id,
'type' => $type,
'amount' => $amount,
'description' => $description,
'status' => $status,
]);
return $mutation;
}
/**
* Retrieve all activities for a given entity.
*/

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('predefined_mutations', function (Blueprint $table) {
$table->id();
$table->foreignId('dynamic_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->text('description')->nullable();
$table->integer('amount');
$table->string('type')->default('reward');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('predefined_mutations');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('mutations', function (Blueprint $table) {
$table->foreignId('predefined_mutation_id')->nullable()->constrained()->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('mutations', function (Blueprint $table) {
$table->dropForeign(['predefined_mutation_id']);
$table->dropColumn('predefined_mutation_id');
});
}
};

View File

@ -7,6 +7,7 @@ use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\Mutation;
use App\Models\Message;
use App\Services\ActivityService;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
@ -16,9 +17,9 @@ class DatabaseSeeder extends Seeder
/**
* Seed the application's database.
*/
public function run(): void
public function run(ActivityService $activityService): void
{
DB::transaction(function () {
DB::transaction(function () use ($activityService) {
// 1. Create Core Users
$testUser = User::factory()->create([
'name' => 'Test User',
@ -52,7 +53,9 @@ class DatabaseSeeder extends Seeder
// ----------------------------------------------------
$velvetSanctuary = Dynamic::create([
'name' => 'The Velvet Sanctuary',
'rules' => "1. Respect limits and boundaries at all times.\n2. Submit daily logs for curfew and chores.\n3. Maintain proper protocol in the general discussion.",
'rules' => "1. Respect limits and boundaries at all times.
2. Submit daily logs for curfew and chores.
3. Maintain proper protocol in the general discussion.",
]);
// Add participants (Test User is owner, Alice is owner, Bob is submissive/participant)
@ -64,29 +67,10 @@ class DatabaseSeeder extends Seeder
$velvetChat = $velvetSanctuary->chat;
// Seed Dynamic Chat Messages
Message::create([
'chat_id' => $velvetChat->id,
'user_id' => $alice->id,
'content' => 'Good morning everyone. Bob, please ensure the Obsidian room is polished before 4 PM.',
]);
Message::create([
'chat_id' => $velvetChat->id,
'user_id' => $testUser->id,
'content' => 'I will review the curfew log later today.',
]);
Message::create([
'chat_id' => $velvetChat->id,
'user_id' => $bob->id,
'content' => "Yes, Sir. Yes, Ma'am. I am starting on the chores now.",
]);
Message::create([
'chat_id' => $velvetChat->id,
'user_id' => $testUser->id,
'content' => 'Excellent. Keep up the high standard.',
]);
$activityService->createMessage($velvetChat, $alice, 'Good morning everyone. Bob, please ensure the Obsidian room is polished before 4 PM.');
$activityService->createMessage($velvetChat, $testUser, 'I will review the curfew log later today.');
$activityService->createMessage($velvetChat, $bob, "Yes, Sir. Yes, Ma'am. I am starting on the chores now.");
$activityService->createMessage($velvetChat, $testUser, 'Excellent. Keep up the high standard.');
// Add Ledgers
$curfewLedger = Ledger::create([
@ -114,145 +98,37 @@ class DatabaseSeeder extends Seeder
]);
// Seed Curfew Mutations
Mutation::create([
'ledger_id' => $curfewLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 10,
'description' => 'Checked in by 10:45 PM on Friday',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $curfewLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 15,
'description' => 'Checked in by 10:30 PM on Saturday',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $curfewLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 10,
'description' => 'Checked in by 10:50 PM on Sunday',
'status' => 'approved',
]);
$activityService->createMutation($curfewLedger, $bob, 'reward', 10, 'Checked in by 10:45 PM on Friday', 'approved');
$activityService->createMutation($curfewLedger, $bob, 'reward', 15, 'Checked in by 10:30 PM on Saturday', 'approved');
$activityService->createMutation($curfewLedger, $bob, 'reward', 10, 'Checked in by 10:50 PM on Sunday', 'approved');
// Seed Cleaning Mutations
Mutation::create([
'ledger_id' => $cleaningLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 15,
'description' => 'Deep cleaning of the main chamber',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $cleaningLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 20,
'description' => 'Arranged gear rack and polished leather accessories',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $cleaningLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => 10,
'description' => 'Mopped obsidian floors',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $cleaningLedger->id,
'user_id' => $alice->id,
'type' => 'penalty',
'amount' => -10,
'description' => 'Left keys in the locks unmonitored',
'status' => 'approved',
]);
$activityService->createMutation($cleaningLedger, $bob, 'reward', 15, 'Deep cleaning of the main chamber', 'approved');
$activityService->createMutation($cleaningLedger, $bob, 'reward', 20, 'Arranged gear rack and polished leather accessories', 'approved');
$activityService->createMutation($cleaningLedger, $bob, 'reward', 10, 'Mopped obsidian floors', 'approved');
$activityService->createMutation($cleaningLedger, $alice, 'penalty', -10, 'Left keys in the locks unmonitored', 'approved');
// Seed Pending Mutation with its own Chat Messages!
$pendingMutation = Mutation::create([
'ledger_id' => $cleaningLedger->id,
'user_id' => $bob->id,
'type' => 'addition',
'amount' => 10,
'description' => 'Weekly chore submission - dusting shelves',
'status' => 'pending',
]);
// Pending mutation chat messages (chat is auto-created on booted)
$pendingMutation = $activityService->createMutation($cleaningLedger, $bob, 'addition', 10, 'Weekly chore submission - dusting shelves', 'pending');
$pendingMutationChat = $pendingMutation->chat;
Message::create([
'chat_id' => $pendingMutationChat->id,
'user_id' => $bob->id,
'content' => 'I have finished the shelves. Please approve when convenient.',
]);
Message::create([
'chat_id' => $pendingMutationChat->id,
'user_id' => $alice->id,
'content' => "I checked them; there is still some dust on the top shelf. I'll leave this pending until it's perfect.",
]);
Message::create([
'chat_id' => $pendingMutationChat->id,
'user_id' => $bob->id,
'content' => 'Apologies, Ma\'am. I will re-wipe the top section immediately!',
]);
$activityService->createMessage($pendingMutationChat, $bob, 'I have finished the shelves. Please approve when convenient.');
$activityService->createMessage($pendingMutationChat, $alice, "I checked them; there is still some dust on the top shelf. I'll leave this pending until it's perfect.");
$activityService->createMessage($pendingMutationChat, $bob, 'Apologies, Ma\'am. I will re-wipe the top section immediately!');
// Seed Etiquette Mutations
Mutation::create([
'ledger_id' => $etiquetteLedger->id,
'user_id' => $alice->id,
'type' => 'penalty',
'amount' => 5,
'description' => 'Interrupted Domina Alice during daily instructions',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $etiquetteLedger->id,
'user_id' => $alice->id,
'type' => 'penalty',
'amount' => 10,
'description' => 'Forgot correct posture during morning roll call',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $etiquetteLedger->id,
'user_id' => $alice->id,
'type' => 'penalty',
'amount' => 5,
'description' => 'Spoke out of turn in general chat',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $etiquetteLedger->id,
'user_id' => $bob->id,
'type' => 'reward',
'amount' => -5,
'description' => 'Excellent reciting of the house codes',
'status' => 'approved',
]);
$activityService->createMutation($etiquetteLedger, $alice, 'penalty', 5, 'Interrupted Domina Alice during daily instructions', 'approved');
$activityService->createMutation($etiquetteLedger, $alice, 'penalty', 10, 'Forgot correct posture during morning roll call', 'approved');
$activityService->createMutation($etiquetteLedger, $alice, 'penalty', 5, 'Spoke out of turn in general chat', 'approved');
$activityService->createMutation($etiquetteLedger, $bob, 'reward', -5, 'Excellent reciting of the house codes', 'approved');
// ----------------------------------------------------
// 3. Seed Dynamic 2: Obsidian Household Agreement
// ----------------------------------------------------
$obsidianHousehold = Dynamic::create([
'name' => 'Obsidian Household Agreement',
'rules' => "1. All residents must do their fair share of maintenance.\n2. Coffee machine must be refilled immediately when empty.",
'rules' => "1. All residents must do their fair share of maintenance.
2. Coffee machine must be refilled immediately when empty.",
]);
$obsidianHousehold->participants()->attach($alice->id, ['role' => 'owner']);
@ -261,29 +137,10 @@ class DatabaseSeeder extends Seeder
$obsidianChat = $obsidianHousehold->chat;
Message::create([
'chat_id' => $obsidianChat->id,
'user_id' => $alice->id,
'content' => "Who finished the coffee beans and didn't put them on the shopping list?",
]);
Message::create([
'chat_id' => $obsidianChat->id,
'user_id' => $charles->id,
'content' => "Wasn't me, I only drink tea.",
]);
Message::create([
'chat_id' => $obsidianChat->id,
'user_id' => $testUser->id,
'content' => 'My apologies! I did refill the hopper, but forgot to list the replacement bag. I will buy a new pack tonight.',
]);
Message::create([
'chat_id' => $obsidianChat->id,
'user_id' => $alice->id,
'content' => 'Thank you, Test User. Appreciate the honesty.',
]);
$activityService->createMessage($obsidianChat, $alice, "Who finished the coffee beans and didn't put them on the shopping list?");
$activityService->createMessage($obsidianChat, $charles, "Wasn't me, I only drink tea.");
$activityService->createMessage($obsidianChat, $testUser, 'My apologies! I did refill the hopper, but forgot to list the replacement bag. I will buy a new pack tonight.');
$activityService->createMessage($obsidianChat, $alice, 'Thank you, Test User. Appreciate the honesty.');
// Add Ledgers
$kitchenLedger = Ledger::create([
@ -303,33 +160,11 @@ class DatabaseSeeder extends Seeder
]);
// Seed Chores Mutations
Mutation::create([
'ledger_id' => $kitchenLedger->id,
'user_id' => $testUser->id,
'type' => 'reward',
'amount' => 25,
'description' => 'Emptied and loaded the dishwasher',
'status' => 'approved',
]);
Mutation::create([
'ledger_id' => $kitchenLedger->id,
'user_id' => $testUser->id,
'type' => 'reward',
'amount' => 15,
'description' => 'Took out recycling and trash bags',
'status' => 'approved',
]);
$activityService->createMutation($kitchenLedger, $testUser, 'reward', 25, 'Emptied and loaded the dishwasher', 'approved');
$activityService->createMutation($kitchenLedger, $testUser, 'reward', 15, 'Took out recycling and trash bags', 'approved');
// Seed Coffee Mutations
Mutation::create([
'ledger_id' => $coffeeLedger->id,
'user_id' => $testUser->id,
'type' => 'reward',
'amount' => 10,
'description' => 'Descaled and refilled coffee beans',
'status' => 'approved',
]);
$activityService->createMutation($coffeeLedger, $testUser, 'reward', 10, 'Descaled and refilled coffee beans', 'approved');
});
}
}

View File

@ -0,0 +1,247 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
import { defineProps } from 'vue';
const props = defineProps<{
dynamic: {
id: number;
name: string;
};
predefined_mutations: Array<{
id: number;
name: string;
description: string;
amount: number;
type: string;
}>;
}>();
const form = useForm({
name: '',
description: '',
amount: 0,
type: 'reward',
});
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: 'Predefined Mutations',
href: route('dynamics.predefined-mutations.index', props.dynamic.id),
},
];
function submit() {
form.post(route('dynamics.predefined-mutations.store', props.dynamic.id), {
onSuccess: () => form.reset(),
});
}
</script>
<template>
<Head title="Predefined Mutations" />
<AppLayout :breadcrumbs="breadcrumbs">
<div class="c-predefined-mutations">
<div class="c-predefined-mutations__container">
<div class="c-predefined-mutations__card">
<div class="c-predefined-mutations__body">
<h3 class="c-predefined-mutations__title">
Predefined Mutations for {{ dynamic.name }}
</h3>
<div class="c-predefined-mutations__list">
<div
v-for="mutation in predefined_mutations"
:key="mutation.id"
class="c-predefined-mutations__item"
>
<div class="c-predefined-mutations__item-details">
<h4 class="c-predefined-mutations__item-name">
{{ mutation.name }}
</h4>
<p class="c-predefined-mutations__item-description">
{{ mutation.description }}
</p>
</div>
<div class="c-predefined-mutations__item-amount">
{{ mutation.amount }}
</div>
</div>
</div>
</div>
</div>
<div class="c-predefined-mutations__card mt-8">
<div class="c-predefined-mutations__body">
<h3 class="c-predefined-mutations__title">
Create New Predefined Mutation
</h3>
<form
@submit.prevent="submit"
class="c-predefined-mutations__form"
>
<div class="c-predefined-mutations__field">
<label
for="name"
class="c-predefined-mutations__label"
>Name</label
>
<input
v-model="form.name"
id="name"
type="text"
class="c-predefined-mutations__input"
/>
</div>
<div class="c-predefined-mutations__field">
<label
for="description"
class="c-predefined-mutations__label"
>Description</label
>
<textarea
v-model="form.description"
id="description"
rows="4"
class="c-predefined-mutations__textarea"
></textarea>
</div>
<div class="c-predefined-mutations__field">
<label
for="amount"
class="c-predefined-mutations__label"
>Amount</label
>
<input
v-model="form.amount"
id="amount"
type="number"
class="c-predefined-mutations__input"
/>
</div>
<div class="c-predefined-mutations__field">
<label
for="type"
class="c-predefined-mutations__label"
>Type</label
>
<select
v-model="form.type"
id="type"
class="c-predefined-mutations__select"
>
<option value="reward">Reward</option>
<option value="penalty">Penalty</option>
</select>
</div>
<div class="c-predefined-mutations__actions">
<button
type="submit"
:disabled="form.processing"
class="c-predefined-mutations__submit-btn"
>
Create
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</AppLayout>
</template>
<style scoped>
@reference "../../../../css/app.css";
.c-predefined-mutations {
@apply py-12;
}
.c-predefined-mutations__container {
@apply mx-auto max-w-7xl sm:px-6 lg:px-8;
}
.c-predefined-mutations__card {
@apply overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800;
}
.c-predefined-mutations__body {
@apply p-6 text-gray-900 dark:text-gray-100;
}
.c-predefined-mutations__title {
@apply text-lg font-medium;
}
.c-predefined-mutations__list {
@apply mt-6 space-y-4;
}
.c-predefined-mutations__item {
@apply flex items-center justify-between rounded-lg border p-4 dark:border-gray-700;
}
.c-predefined-mutations__item-details {
@apply flex-1;
}
.c-predefined-mutations__item-name {
@apply font-semibold;
}
.c-predefined-mutations__item-description {
@apply text-sm text-gray-600 dark:text-gray-400;
}
.c-predefined-mutations__item-amount {
@apply text-lg font-semibold;
}
.c-predefined-mutations__form {
@apply mt-6 space-y-6;
}
.c-predefined-mutations__field {
@apply block;
}
.c-predefined-mutations__label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
}
.c-predefined-mutations__input {
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
}
.c-predefined-mutations__textarea {
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
}
.c-predefined-mutations__select {
@apply mt-1 block w-full rounded-md border-gray-300 bg-white p-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
}
.c-predefined-mutations__actions {
@apply flex items-center gap-4;
}
.c-predefined-mutations__submit-btn {
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { Head, useForm, Link as InertiaLink } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
@ -70,6 +70,12 @@ function submit() {
</form>
</div>
</div>
<div class="mt-8">
<InertiaLink :href="route('dynamics.predefined-mutations.index', dynamic.id)" class="c-dynamic-settings__submit-btn">
Manage Predefined Mutations
</InertiaLink>
</div>
</div>
</div>
</template>

View File

@ -19,6 +19,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dynamics/{dynamic}/ledgers/create', [LedgerController::class, 'create'])->name('dynamics.ledgers.create');
Route::resource('dynamics.ledgers', LedgerController::class)->scoped()->except(['create']);
Route::resource('dynamics.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped();
Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped();
Route::get('/dynamics/{dynamic}/invitations/create', [\App\Http\Controllers\DynamicInvitationController::class, 'create'])->name('dynamics.invitations.create');