diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 0138c0d..c4b1c82 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -11,10 +11,10 @@ class DashboardController extends Controller public function index(Request $request, ActivityService $activityService) { $user = $request->user(); - $unreadEntities = $activityService->getUnreadEntitiesGrouped($user); + $unreadDynamics = $activityService->getUnreadDynamicsGrouped($user); return Inertia::render('Dashboard', [ - 'unreadEntities' => $unreadEntities, + 'unreadDynamics' => $unreadDynamics, ]); } } diff --git a/app/Http/Controllers/DynamicController.php b/app/Http/Controllers/DynamicController.php index 00d581f..3fef526 100644 --- a/app/Http/Controllers/DynamicController.php +++ b/app/Http/Controllers/DynamicController.php @@ -2,8 +2,10 @@ namespace App\Http\Controllers; -use App\Http\Requests\StoreDynamicRequest; -use App\Http\Requests\UpdateDynamicRequest; +use App\Http\Resources\DynamicResource; +use App\Http\Resources\LedgerResource; +use App\Http\Resources\MessageResource; +use App\Http\Resources\UserResource; use App\Models\Dynamic; use App\Services\ActivityService; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -19,7 +21,7 @@ class DynamicController extends Controller public function index(Request $request) { return Inertia::render('Dynamics/Index', [ - 'dynamics' => $request->user()->dynamics()->get(), + 'dynamics' => DynamicResource::collection($request->user()->dynamics()->get()), ]); } @@ -54,15 +56,14 @@ class DynamicController extends Controller $dynamic->load(['ledgers.media', 'participants', 'chat']); - $isOwner = $dynamic->participants() - ->where('user_id', $request->user()->id) - ->where('role', 'owner') - ->exists(); - return Inertia::render('Dynamics/Show', [ - 'dynamic' => $dynamic, - 'isOwner' => $isOwner, - 'messages' => $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20), + 'dynamic' => new DynamicResource($dynamic), + 'ledgers' => LedgerResource::collection($dynamic->ledgers), + 'participants' => UserResource::collection($dynamic->participants), + 'messages' => MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)), + 'can' => [ + 'update' => $request->user()->can('update', $dynamic), + ], ]); } @@ -70,7 +71,7 @@ class DynamicController extends Controller { $this->authorize('view', $dynamic); - return $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20); + return MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)); } /** @@ -81,7 +82,7 @@ class DynamicController extends Controller $this->authorize('update', $dynamic); return Inertia::render('Dynamics/Settings', [ - 'dynamic' => $dynamic, + 'dynamic' => new DynamicResource($dynamic), ]); } diff --git a/app/Http/Controllers/DynamicInvitationController.php b/app/Http/Controllers/DynamicInvitationController.php index a39714f..6d35a74 100644 --- a/app/Http/Controllers/DynamicInvitationController.php +++ b/app/Http/Controllers/DynamicInvitationController.php @@ -125,6 +125,6 @@ class DynamicInvitationController extends Controller $invitation->delete(); }); - return redirect()->route('dynamics.show', $invitation->dynamic_id)->with('success', 'Successfully joined the dynamic!'); + return redirect()->route('dynamics.show', $invitation->dynamic)->with('success', 'Successfully joined the dynamic!'); } } diff --git a/app/Http/Controllers/LedgerController.php b/app/Http/Controllers/LedgerController.php index b428dab..c9ac0cb 100644 --- a/app/Http/Controllers/LedgerController.php +++ b/app/Http/Controllers/LedgerController.php @@ -3,6 +3,11 @@ namespace App\Http\Controllers; use App\Http\Requests\StoreLedgerRequest; +use App\Http\Resources\DynamicResource; +use App\Http\Resources\LedgerResource; +use App\Http\Resources\MessageResource; +use App\Http\Resources\MutationResource; +use App\Http\Resources\UserResource; use App\Models\Dynamic; use App\Models\Ledger; use App\Services\ActivityService; @@ -30,7 +35,7 @@ class LedgerController extends Controller $this->authorize('update', $dynamic); return Inertia::render('Ledgers/Create', [ - 'dynamic' => $dynamic, + 'dynamic' => new DynamicResource($dynamic), ]); } @@ -39,6 +44,7 @@ class LedgerController extends Controller */ public function store(StoreLedgerRequest $request, Dynamic $dynamic) { + $this->authorize('create', [Ledger::class, $dynamic]); $ledger = $dynamic->ledgers()->create($request->except('media')); if ($request->hasFile('media')) { @@ -76,16 +82,16 @@ class LedgerController extends Controller 'mutations.chat', ]); - $isOwner = $dynamic->participants() - ->where('user_id', $request->user()->id) - ->where('role', 'owner') - ->exists(); - return Inertia::render('Ledgers/Show', [ - 'dynamic' => $dynamic, - 'ledger' => $ledger, - 'isOwner' => $isOwner, - 'messages' => $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20), + 'dynamic' => new DynamicResource($dynamic), + 'ledger' => new LedgerResource($ledger), + 'mutations' => MutationResource::collection($ledger->mutations), + 'participants' => UserResource::collection($dynamic->participants), + 'messages' => MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)), + 'can' => [ + 'update' => $request->user()->can('update', $ledger), + 'close' => $request->user()->can('close', $ledger), + ], ]); } @@ -93,23 +99,41 @@ class LedgerController extends Controller { $this->authorize('view', $ledger); - return $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20); + return MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)); } /** * Show the form for editing the specified resource. */ - public function edit(Ledger $ledger) + public function edit(Dynamic $dynamic, Ledger $ledger) { - // + $this->authorize('update', $ledger); + + return Inertia::render('Ledgers/Edit', [ + 'dynamic' => new DynamicResource($dynamic), + 'ledger' => new LedgerResource($ledger), + ]); } /** * Update the specified resource in storage. */ - public function update(Request $request, Ledger $ledger) + public function update(StoreLedgerRequest $request, Dynamic $dynamic, Ledger $ledger) { - // + $this->authorize('update', $ledger); + + $ledger->update($request->validated()); + + return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]); + } + + public function close(Request $request, Dynamic $dynamic, Ledger $ledger) + { + $this->authorize('close', $ledger); + + $ledger->update(['status' => 'closed']); + + return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]); } /** diff --git a/app/Http/Controllers/MutationController.php b/app/Http/Controllers/MutationController.php index 5d31afd..65da010 100644 --- a/app/Http/Controllers/MutationController.php +++ b/app/Http/Controllers/MutationController.php @@ -2,15 +2,19 @@ namespace App\Http\Controllers; +use App\Http\Resources\MutationResource; use App\Http\Requests\StoreMutationRequest; use App\Models\Dynamic; use App\Models\Ledger; use App\Models\Mutation; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; class MutationController extends Controller { + use AuthorizesRequests; + /** * Display a listing of the resource. */ @@ -32,13 +36,10 @@ class MutationController extends Controller */ public function store(StoreMutationRequest $request, Dynamic $dynamic, Ledger $ledger) { - $isOwner = $dynamic->participants() - ->where('user_id', $request->user()->id) - ->where('role', 'owner') - ->exists(); + $this->authorize('create', [Mutation::class, $ledger]); // If the user is an owner, default status to 'approved'. Otherwise default to 'pending'. - $status = $isOwner ? 'approved' : 'pending'; + $status = $request->user()->can('update', $ledger) ? 'approved' : 'pending'; $mutation = DB::transaction(function () use ($request, $ledger, $status) { $mutation = $ledger->mutations()->create([ @@ -78,7 +79,9 @@ class MutationController extends Controller */ public function show(Dynamic $dynamic, Ledger $ledger, Mutation $mutation) { - // + $this->authorize('view', $mutation); + + return new MutationResource($mutation); } /** @@ -94,15 +97,7 @@ class MutationController extends Controller */ public function update(Request $request, Dynamic $dynamic, Ledger $ledger, Mutation $mutation) { - // 1. Authorize - only owners can update mutation status! - $isOwner = $dynamic->participants() - ->where('user_id', $request->user()->id) - ->where('role', 'owner') - ->exists(); - - if (!$isOwner) { - abort(403, 'Only dynamic owners can approve or reject mutations.'); - } + $this->authorize('update', $mutation); $request->validate([ 'status' => ['required', 'string', 'in:approved,rejected'], @@ -157,6 +152,15 @@ class MutationController extends Controller return redirect()->back(); } + public function void(Request $request, Dynamic $dynamic, Ledger $ledger, Mutation $mutation) + { + $this->authorize('void', $mutation); + + $mutation->update(['status' => 'voided']); + + return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]); + } + /** * Remove the specified resource from storage. */ diff --git a/app/Http/Controllers/PredefinedMutationController.php b/app/Http/Controllers/PredefinedMutationController.php index e669fcd..ae02836 100644 --- a/app/Http/Controllers/PredefinedMutationController.php +++ b/app/Http/Controllers/PredefinedMutationController.php @@ -16,21 +16,20 @@ class PredefinedMutationController extends Controller /** * Display a listing of the resource. */ - public function index(Dynamic $dynamic, Ledger $ledger) + public function index(Dynamic $dynamic) { $this->authorize('update', $dynamic); - return Inertia::render('Ledgers/PredefinedMutations/Index', [ + return Inertia::render('Dynamics/PredefinedMutations/Index', [ 'dynamic' => $dynamic, - 'ledger' => $ledger, - 'predefined_mutations' => $ledger->predefinedMutations()->latest()->get(), + 'predefined_mutations' => $dynamic->predefinedMutations()->latest()->get(), ]); } /** * Store a newly created resource in storage. */ - public function store(Request $request, Dynamic $dynamic, Ledger $ledger) + public function store(Request $request, Dynamic $dynamic) { $this->authorize('update', $dynamic); @@ -38,10 +37,55 @@ class PredefinedMutationController extends Controller 'name' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'amount' => ['required', 'integer'], + 'type' => ['required', 'string', 'in:reward,penalty'], ]); - $ledger->predefinedMutations()->create($request->all()); + $dynamic->predefinedMutations()->create($request->all()); - return redirect()->route('dynamics.ledgers.predefined-mutations.index', [$dynamic, $ledger]); + return redirect()->route('dynamics.predefined-mutations.index', $dynamic); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Dynamic $dynamic, PredefinedMutation $predefinedMutation) + { + $this->authorize('update', $dynamic); + + return Inertia::render('Dynamics/PredefinedMutations/Edit', [ + 'dynamic' => $dynamic, + 'predefined_mutation' => $predefinedMutation, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Dynamic $dynamic, PredefinedMutation $predefinedMutation) + { + $this->authorize('update', $dynamic); + + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'amount' => ['required', 'integer'], + 'type' => ['required', 'string', 'in:reward,penalty'], + ]); + + $predefinedMutation->update($request->all()); + + return redirect()->route('dynamics.predefined-mutations.index', $dynamic); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Dynamic $dynamic, PredefinedMutation $predefinedMutation) + { + $this->authorize('update', $dynamic); + + $predefinedMutation->delete(); + + return redirect()->route('dynamics.predefined-mutations.index', $dynamic); } } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 987e593..0043228 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -48,7 +48,7 @@ class HandleInertiaRequests extends Middleware } $service = app(\App\Services\ActivityService::class); - return count($service->getUnreadEntitiesGrouped($request->user())); + return count($service->getUnreadDynamicsGrouped($request->user())); }, ]; } diff --git a/app/Http/Resources/BaseResource.php b/app/Http/Resources/BaseResource.php new file mode 100644 index 0000000..1db8906 --- /dev/null +++ b/app/Http/Resources/BaseResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + $data = parent::toArray($request); + + if (isset($data['id']) && isset($this->uuid)) { + $data['id'] = $this->uuid; + } + + return $data; + } +} diff --git a/app/Http/Resources/DynamicResource.php b/app/Http/Resources/DynamicResource.php new file mode 100644 index 0000000..69b9069 --- /dev/null +++ b/app/Http/Resources/DynamicResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/LedgerResource.php b/app/Http/Resources/LedgerResource.php new file mode 100644 index 0000000..1bc420f --- /dev/null +++ b/app/Http/Resources/LedgerResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/MessageResource.php b/app/Http/Resources/MessageResource.php new file mode 100644 index 0000000..5e11358 --- /dev/null +++ b/app/Http/Resources/MessageResource.php @@ -0,0 +1,18 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/MutationResource.php b/app/Http/Resources/MutationResource.php new file mode 100644 index 0000000..2ccdcbf --- /dev/null +++ b/app/Http/Resources/MutationResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/PredefinedMutationResource.php b/app/Http/Resources/PredefinedMutationResource.php new file mode 100644 index 0000000..6bb4e96 --- /dev/null +++ b/app/Http/Resources/PredefinedMutationResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..45b2b35 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Models/Dynamic.php b/app/Models/Dynamic.php index 75f41e4..7cb68f3 100644 --- a/app/Models/Dynamic.php +++ b/app/Models/Dynamic.php @@ -34,6 +34,11 @@ class Dynamic extends Model return $this->hasMany(DynamicInvitation::class); } + public function predefinedMutations(): HasMany + { + return $this->hasMany(PredefinedMutation::class); + } + public function chat(): MorphOne { return $this->morphOne(Chat::class, 'chatable'); @@ -41,8 +46,17 @@ class Dynamic extends Model protected static function booted(): void { + static::creating(function ($model) { + $model->uuid = (string) \Illuminate\Support\Str::uuid(); + }); + static::created(function (Dynamic $dynamic) { $dynamic->chat()->create([]); }); } + + public function getRouteKeyName() + { + return 'uuid'; + } } diff --git a/app/Models/Ledger.php b/app/Models/Ledger.php index 22ce39b..97d9b34 100644 --- a/app/Models/Ledger.php +++ b/app/Models/Ledger.php @@ -19,6 +19,7 @@ class Ledger extends Model 'rules', 'score', 'alignment', + 'status', ]; public function dynamic(): BelongsTo @@ -40,4 +41,16 @@ class Ledger extends Model { return $this->morphMany(Media::class, 'mediable'); } + + protected static function booted(): void + { + static::creating(function ($model) { + $model->uuid = (string) \Illuminate\Support\Str::uuid(); + }); + } + + public function getRouteKeyName() + { + return 'uuid'; + } } diff --git a/app/Models/Mutation.php b/app/Models/Mutation.php index 67431c9..0c512de 100644 --- a/app/Models/Mutation.php +++ b/app/Models/Mutation.php @@ -50,6 +50,10 @@ class Mutation extends Model protected static function booted(): void { + static::creating(function ($model) { + $model->uuid = (string) \Illuminate\Support\Str::uuid(); + }); + static::created(function (Mutation $mutation) { $mutation->chat()->create([]); @@ -87,4 +91,9 @@ class Mutation extends Model broadcast(new \App\Events\MessageSent($dynamicMsg)); }); } + + public function getRouteKeyName() + { + return 'uuid'; + } } diff --git a/app/Models/PredefinedMutation.php b/app/Models/PredefinedMutation.php index a3eb740..0ab9ae4 100644 --- a/app/Models/PredefinedMutation.php +++ b/app/Models/PredefinedMutation.php @@ -11,14 +11,27 @@ class PredefinedMutation extends Model use HasFactory; protected $fillable = [ - 'ledger_id', + 'dynamic_id', 'name', 'description', 'amount', + 'type', ]; - public function ledger(): BelongsTo + public function dynamic(): BelongsTo { - return $this->belongsTo(Ledger::class); + return $this->belongsTo(Dynamic::class); + } + + protected static function booted(): void + { + static::creating(function ($model) { + $model->uuid = (string) \Illuminate\Support\Str::uuid(); + }); + } + + public function getRouteKeyName() + { + return 'uuid'; } } diff --git a/app/Models/User.php b/app/Models/User.php index afcd538..73840d5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -69,4 +69,16 @@ class User extends Authenticatable implements PasskeyUser 'two_factor_confirmed_at' => 'datetime', ]; } + + protected static function booted(): void + { + static::creating(function ($model) { + $model->uuid = (string) \Illuminate\Support\Str::uuid(); + }); + } + + public function getRouteKeyName() + { + return 'uuid'; + } } diff --git a/app/Policies/LedgerPolicy.php b/app/Policies/LedgerPolicy.php index 8b1f66e..05bc736 100644 --- a/app/Policies/LedgerPolicy.php +++ b/app/Policies/LedgerPolicy.php @@ -37,7 +37,15 @@ class LedgerPolicy */ public function update(User $user, Ledger $ledger): bool { - return false; + return $user->can('update', $ledger->dynamic); + } + + /** + * Determine whether the user can close the model. + */ + public function close(User $user, Ledger $ledger): bool + { + return $user->can('update', $ledger->dynamic); } /** diff --git a/app/Policies/MutationPolicy.php b/app/Policies/MutationPolicy.php index e6288e8..dd29e07 100644 --- a/app/Policies/MutationPolicy.php +++ b/app/Policies/MutationPolicy.php @@ -2,18 +2,39 @@ namespace App\Policies; +use App\Models\Ledger; use App\Models\Mutation; use App\Models\User; class MutationPolicy { /** - * Determine whether the user can view the mutation. + * Determine whether the user can create mutations. */ - public function view(User $user, Mutation $mutation): bool + public function create(User $user, Ledger $ledger): bool { - $dynamic = $mutation->ledger->dynamic; + $dynamic = $ledger->dynamic; return $dynamic->participants()->where('user_id', $user->id)->exists(); } + + /** + * Determine whether the user can update the mutation. + */ + public function update(User $user, Mutation $mutation): bool + { + $dynamic = $mutation->ledger->dynamic; + + return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists(); + } + + /** + * Determine whether the user can void the mutation. + */ + public function void(User $user, Mutation $mutation): bool + { + $dynamic = $mutation->ledger->dynamic; + + return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists(); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f1525e9..7d7b56a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; @@ -23,6 +24,7 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + JsonResource::withoutWrapping(); $this->configureDefaults(); } diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 7b099a3..5a94246 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -73,25 +73,15 @@ class ActivityService /** * Retrieve all activities for a given entity. */ - public function getActivitiesForEntity($entity): array + public function getActivitiesForDynamic(Dynamic $dynamic): array { - if ($entity instanceof Dynamic) { - $chatId = $entity->chat->id; - $dynamic = $entity; - } elseif ($entity instanceof Ledger) { - $dynamic = $entity->dynamic; - $chatId = $dynamic->chat->id; - } else { - return []; - } - $participants = $dynamic->participants()->withPivot('display_name')->get(); $participantsMap = $participants->reduce(function ($acc, $p) { $acc[$p->id] = $p->pivot->display_name ?? $p->name; return $acc; }, []); - $messages = Message::where('chat_id', $chatId) + $messages = Message::where('chat_id', $dynamic->chat->id) ->with(['user', 'subject']) ->latest() ->get(); @@ -113,27 +103,25 @@ class ActivityService /** * Get unread activities grouped by active entities (Dynamics, Ledgers) for the given user. */ - public function getUnreadEntitiesGrouped(User $user): array + public function getUnreadDynamicsGrouped(User $user): array { - $groupedEntities = []; + $groupedDynamics = []; $participatingDynamics = $user->dynamics()->with('ledgers')->get(); - $entities = $participatingDynamics->concat($participatingDynamics->flatMap(fn ($d) => $d->ledgers)); + foreach ($participatingDynamics as $dynamic) { + $readAt = $this->getCursorReadAt($user, $dynamic); + $activities = $this->getActivitiesForDynamic($dynamic); - foreach ($entities as $entity) { - $readAt = $this->getCursorReadAt($user, $entity); - $activities = $this->getActivitiesForEntity($entity); - - $this->partitionActivities($activities, $readAt, $entity, get_class($entity), $this->getUrlForEntity($entity), $groupedEntities); + $this->partitionAndGroupActivities($activities, $readAt, $dynamic, $groupedDynamics); } - return $groupedEntities; + return $groupedDynamics; } /** * Partition activities into read and unread, and construct the grouped entity metadata. */ - private function partitionActivities(array $activities, \Carbon\CarbonInterface $readAt, $entity, string $type, string $url, array &$groupedEntities): void + private function partitionAndGroupActivities(array $activities, \Carbon\CarbonInterface $readAt, Dynamic $dynamic, array &$groupedDynamics): void { $alreadyRead = []; $unread = []; @@ -149,11 +137,10 @@ class ActivityService if (!empty($unread)) { $context = array_slice($alreadyRead, 0, 2); - $groupedEntities[] = [ - 'id' => $entity->id, - 'name' => $entity->name, - 'type' => Str::afterLast($type, '\\'), - 'url' => $url, + $groupedDynamics[] = [ + 'id' => $dynamic->id, + 'name' => $dynamic->name, + 'url' => route('dynamics.show', $dynamic->uuid), 'unread_count' => count($unread), 'context_activities' => $context, 'new_activities' => array_reverse($unread), @@ -164,11 +151,11 @@ class ActivityService private function getUrlForEntity($entity): string { if ($entity instanceof Dynamic) { - return route('dynamics.show', $entity->id); + return route('dynamics.show', $entity->uuid); } if ($entity instanceof Ledger) { - return route('dynamics.ledgers.show', [$entity->dynamic_id, $entity->id]); + return route('dynamics.ledgers.show', [$entity->dynamic->uuid, $entity->uuid]); } return ''; diff --git a/database/migrations/2026_06_14_222648_create_mutations_table.php b/database/migrations/2026_06_14_222648_create_mutations_table.php index 366e87d..a273833 100644 --- a/database/migrations/2026_06_14_222648_create_mutations_table.php +++ b/database/migrations/2026_06_14_222648_create_mutations_table.php @@ -18,7 +18,7 @@ return new class extends Migration $table->string('type'); $table->integer('amount'); $table->text('description')->nullable(); - $table->string('status')->default('pending'); + $table->string('status')->default('pending'); // pending, approved, rejected, voided $table->timestamps(); }); } diff --git a/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php b/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php index 787c51d..1f157e0 100644 --- a/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php +++ b/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php @@ -13,10 +13,11 @@ return new class extends Migration { Schema::create('predefined_mutations', function (Blueprint $table) { $table->id(); - $table->foreignId('ledger_id')->constrained()->cascadeOnDelete(); + $table->foreignId('dynamic_id')->constrained()->cascadeOnDelete(); $table->string('name'); $table->text('description')->nullable(); $table->integer('amount'); + $table->string('type')->default('reward'); $table->timestamps(); }); } diff --git a/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php b/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php new file mode 100644 index 0000000..28426b3 --- /dev/null +++ b/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php @@ -0,0 +1,28 @@ +string('status')->default('open'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ledgers', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +}; diff --git a/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php b/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php new file mode 100644 index 0000000..029ac02 --- /dev/null +++ b/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php @@ -0,0 +1,28 @@ +string('status')->default('pending')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mutations', function (Blueprint $table) { + $table->string('status')->default('pending')->change(); + }); + } +}; diff --git a/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php b/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php new file mode 100644 index 0000000..d6b5f33 --- /dev/null +++ b/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php @@ -0,0 +1,38 @@ +uuid('uuid')->after('id')->nullable(); + }); + + // Populate existing rows with UUIDs + \App\Models\Dynamic::all()->each(function ($model) { + $model->uuid = \Illuminate\Support\Str::uuid(); + $model->save(); + }); + + Schema::table('dynamics', function (Blueprint $table) { + $table->uuid('uuid')->unique()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('dynamics', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php b/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php new file mode 100644 index 0000000..c1b76b2 --- /dev/null +++ b/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php @@ -0,0 +1,38 @@ +uuid('uuid')->after('id')->nullable(); + }); + + // Populate existing rows with UUIDs + \App\Models\Ledger::all()->each(function ($model) { + $model->uuid = \Illuminate\Support\Str::uuid(); + $model->save(); + }); + + Schema::table('ledgers', function (Blueprint $table) { + $table->uuid('uuid')->unique()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ledgers', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php b/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php new file mode 100644 index 0000000..c1baf5c --- /dev/null +++ b/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php @@ -0,0 +1,38 @@ +uuid('uuid')->after('id')->nullable(); + }); + + // Populate existing rows with UUIDs + \App\Models\Mutation::all()->each(function ($model) { + $model->uuid = \Illuminate\Support\Str::uuid(); + $model->save(); + }); + + Schema::table('mutations', function (Blueprint $table) { + $table->uuid('uuid')->unique()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mutations', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php b/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php new file mode 100644 index 0000000..5c4e347 --- /dev/null +++ b/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php @@ -0,0 +1,38 @@ +uuid('uuid')->after('id')->nullable(); + }); + + // Populate existing rows with UUIDs + \App\Models\PredefinedMutation::all()->each(function ($model) { + $model->uuid = \Illuminate\Support\Str::uuid(); + $model->save(); + }); + + Schema::table('predefined_mutations', function (Blueprint $table) { + $table->uuid('uuid')->unique()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('predefined_mutations', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +}; diff --git a/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php b/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php new file mode 100644 index 0000000..b657b76 --- /dev/null +++ b/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php @@ -0,0 +1,38 @@ +uuid('uuid')->after('id')->nullable(); + }); + + // Populate existing rows with UUIDs + \App\Models\User::all()->each(function ($model) { + $model->uuid = \Illuminate\Support\Str::uuid(); + $model->save(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->uuid('uuid')->unique()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +}; diff --git a/resources/js/components/MutationList.vue b/resources/js/components/MutationList.vue index 561eb64..49d0252 100644 --- a/resources/js/components/MutationList.vue +++ b/resources/js/components/MutationList.vue @@ -40,6 +40,16 @@ function updateStatus(mutationId: number, status: 'approved' | 'rejected') { ); } +function voidMutation(mutationId: number) { + useForm({}).put( + route('dynamics.ledgers.mutations.void', { + dynamic: props.dynamicId, + ledger: props.ledgerId, + mutation: mutationId, + }), + ); +} + function isOwnerUser(userId: number): boolean { const participant = props.participants?.find((p) => p.id === userId); @@ -157,21 +167,30 @@ function getAmountClass(amount: number): string {
+
@@ -290,6 +309,10 @@ function getAmountClass(amount: number): string { @apply inline-flex cursor-pointer items-center rounded bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-500; } +.c-mutation-list__void-btn { + @apply inline-flex cursor-pointer items-center rounded bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-gray-500; +} + .c-mutation-list__empty { @apply mt-4 text-gray-500; } diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index e691057..fbdb728 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -14,10 +14,9 @@ defineOptions({ }); defineProps<{ - unreadEntities: Array<{ + unreadDynamics: Array<{ id: number; name: string; - type: 'Dynamic' | 'Ledger'; url: string; unread_count: number; context_activities: Array<{ @@ -51,34 +50,27 @@ function formatTime(isoString: string): string {

Recent Activity

-
+
- - {{ entity.type }} + + Dynamic - {{ entity.unread_count }} New + {{ dynamic.unread_count }} New

- {{ entity.name }} + {{ dynamic.name }}

@@ -86,7 +78,7 @@ function formatTime(isoString: string): string {
@@ -107,7 +99,7 @@ function formatTime(isoString: string): string {
diff --git a/resources/js/pages/Dynamics/Index.vue b/resources/js/pages/Dynamics/Index.vue index c80a68d..5a54faa 100644 --- a/resources/js/pages/Dynamics/Index.vue +++ b/resources/js/pages/Dynamics/Index.vue @@ -1,10 +1,14 @@ - + + + + diff --git a/resources/js/pages/Dynamics/PredefinedMutations/Index.vue b/resources/js/pages/Dynamics/PredefinedMutations/Index.vue new file mode 100644 index 0000000..33000be --- /dev/null +++ b/resources/js/pages/Dynamics/PredefinedMutations/Index.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/resources/js/pages/Ledgers/Edit.vue b/resources/js/pages/Ledgers/Edit.vue new file mode 100644 index 0000000..372db7f --- /dev/null +++ b/resources/js/pages/Ledgers/Edit.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/resources/js/pages/Ledgers/Show.vue b/resources/js/pages/Ledgers/Show.vue index e191d4a..e9562e3 100644 --- a/resources/js/pages/Ledgers/Show.vue +++ b/resources/js/pages/Ledgers/Show.vue @@ -212,6 +212,12 @@ function isOwnerUser(userId: number): boolean { > Predefined Mutations + + Edit Ledger +
diff --git a/routes/web.php b/routes/web.php index 2fc2830..f577bce 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,10 +19,12 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/dynamics/{dynamic}/ledgers/create', [LedgerController::class, 'create'])->name('dynamics.ledgers.create'); Route::get('/dynamics/{dynamic}/ledgers/{ledger}/messages', [LedgerController::class, 'messages'])->name('dynamics.ledgers.messages'); + Route::put('/dynamics/{dynamic}/ledgers/{ledger}/close', [LedgerController::class, 'close'])->name('dynamics.ledgers.close'); Route::resource('dynamics.ledgers', LedgerController::class)->scoped()->except(['create']); - Route::resource('dynamics.ledgers.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped(); + Route::resource('dynamics.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped(); + Route::put('/dynamics/{dynamic}/ledgers/{ledger}/mutations/{mutation}/void', [\App\Http\Controllers\MutationController::class, 'void'])->name('dynamics.ledgers.mutations.void'); Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped(); Route::get('/dynamics/{dynamic}/invitations/create', [\App\Http\Controllers\DynamicInvitationController::class, 'create'])->name('dynamics.invitations.create'); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php index 65cceeb..3fa35db 100644 --- a/tests/Feature/DashboardTest.php +++ b/tests/Feature/DashboardTest.php @@ -19,7 +19,7 @@ test('authenticated users can visit the dashboard', function () { $response = $this->get(route('dashboard')); $response->assertOk(); - $response->assertInertia(fn ($page) => $page->component('Dashboard')->has('unreadEntities')); + $response->assertInertia(fn ($page) => $page->component('Dashboard')->has('unreadDynamics')); }); test('visiting dynamic updates the read cursor', function () { @@ -34,7 +34,7 @@ test('visiting dynamic updates the read cursor', function () { expect($initialCursor->toIso8601String())->toBe('1970-01-01T00:00:00+00:00'); // Visit Dynamic Show - $this->get(route('dynamics.show', $dynamic->id))->assertOk(); + $this->get(route('dynamics.show', $dynamic->uuid))->assertOk(); // Re-check cursor is updated to near now $updatedCursor = $service->getCursorReadAt($user, $dynamic); @@ -56,7 +56,7 @@ test('visiting ledger updates the read cursor', function () { expect($initialCursor->toIso8601String())->toBe('1970-01-01T00:00:00+00:00'); // Visit Ledger Show - $this->get(route('dynamics.ledgers.show', [$dynamic->id, $ledger->id]))->assertOk(); + $this->get(route('dynamics.ledgers.show', [$dynamic->uuid, $ledger->uuid]))->assertOk(); // Re-check cursor is updated to near now $updatedCursor = $service->getCursorReadAt($user, $ledger); @@ -101,23 +101,23 @@ test('dashboard groups and filters unread entities correctly based on cursor', f // Verify unread grouping structure $response->assertInertia(fn ($page) => $page ->component('Dashboard') - ->where('unreadEntities.0.name', 'Testing Dynamic') - ->where('unreadEntities.0.unread_count', 1) - ->has('unreadEntities.0.context_activities', 1) // Should have old message as context - ->where('unreadEntities.0.context_activities.0.content', 'Old message context') - ->has('unreadEntities.0.new_activities', 1) // Should have unread message - ->where('unreadEntities.0.new_activities.0.content', 'New unread message alert') + ->where('unreadDynamics.0.name', 'Testing Dynamic') + ->where('unreadDynamics.0.unread_count', 1) + ->has('unreadDynamics.0.context_activities', 1) // Should have old message as context + ->where('unreadDynamics.0.context_activities.0.content', 'Old message context') + ->has('unreadDynamics.0.new_activities', 1) // Should have unread message + ->where('unreadDynamics.0.new_activities.0.content', 'New unread message alert') ); // Now visit the Dynamic, which clears the unread count - $this->get(route('dynamics.show', $dynamic->id))->assertOk(); + $this->get(route('dynamics.show', $dynamic->uuid))->assertOk(); // Dashboard should now show 0 unread groups (caught up) $response2 = $this->get(route('dashboard')); $response2->assertOk(); $response2->assertInertia(fn ($page) => $page ->component('Dashboard') - ->has('unreadEntities', 0) + ->has('unreadDynamics', 0) ); Carbon::setTestNow(); // Reset test time diff --git a/tests/Feature/InvitationTest.php b/tests/Feature/InvitationTest.php index 290a96f..8a56555 100644 --- a/tests/Feature/InvitationTest.php +++ b/tests/Feature/InvitationTest.php @@ -19,7 +19,7 @@ test('only owners can invite other users to a dynamic', function () { // 1. Participant tries to send an invite (forbidden) $response = $this->actingAs($participant) - ->post(route('dynamics.invitations.store', $dynamic), [ + ->post(route('dynamics.invitations.store', $dynamic->uuid), [ 'email' => 'invitee@example.com', 'role' => 'participant', ]); @@ -29,7 +29,7 @@ test('only owners can invite other users to a dynamic', function () { // 2. Owner sends a valid invite (allowed) $response = $this->actingAs($owner) - ->post(route('dynamics.invitations.store', $dynamic), [ + ->post(route('dynamics.invitations.store', $dynamic->uuid), [ 'email' => 'invitee@example.com', 'role' => 'participant', ]); @@ -83,7 +83,7 @@ test('only the user with the specified email address can accept the link', funct // 3. Intended user accepts the signed invitation link (success) $response = $this->actingAs($invitee)->get($signedUrl); - $response->assertRedirect(route('dynamics.show', $dynamic)); + $response->assertRedirect(route('dynamics.show', $dynamic->uuid)); // Verify invitee is joined as a participant with the specified role $isJoined = $dynamic->participants() diff --git a/tests/Feature/LedgerTest.php b/tests/Feature/LedgerTest.php index c2636f4..09a9aa8 100644 --- a/tests/Feature/LedgerTest.php +++ b/tests/Feature/LedgerTest.php @@ -12,17 +12,17 @@ test('dynamic owners can view ledger creation form and create ledgers', function $this->actingAs($owner); // Can view form - $this->get(route('dynamics.ledgers.create', $dynamic->id))->assertOk(); + $this->get(route('dynamics.ledgers.create', $dynamic->uuid))->assertOk(); // Can store ledger - $response = $this->post(route('dynamics.ledgers.store', $dynamic->id), [ + $response = $this->post(route('dynamics.ledgers.store', $dynamic->uuid), [ 'name' => 'Chores Ledger', 'rules' => 'Do the tasks.', 'alignment' => 'positive', ]); $response->assertSessionHasNoErrors(); - $response->assertRedirect(route('dynamics.show', $dynamic->id)); + $response->assertRedirect(route('dynamics.show', $dynamic->uuid)); $this->assertDatabaseHas('ledgers', [ 'dynamic_id' => $dynamic->id, @@ -41,10 +41,10 @@ test('non-owners cannot view ledger creation form or store ledgers', function () $this->actingAs($participant); // Cannot view form - $this->get(route('dynamics.ledgers.create', $dynamic->id))->assertStatus(403); + $this->get(route('dynamics.ledgers.create', $dynamic->uuid))->assertStatus(403); // Cannot store ledger - $response = $this->post(route('dynamics.ledgers.store', $dynamic->id), [ + $response = $this->post(route('dynamics.ledgers.store', $dynamic->uuid), [ 'name' => 'Illegal Ledger', 'rules' => 'This should fail.', 'alignment' => 'positive', diff --git a/tests/Feature/ParticipantDetailTest.php b/tests/Feature/ParticipantDetailTest.php index 185cfcc..61b929c 100644 --- a/tests/Feature/ParticipantDetailTest.php +++ b/tests/Feature/ParticipantDetailTest.php @@ -15,7 +15,7 @@ test('authenticated participant can view another participant detail page in dyna $this->actingAs($owner); - $response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id])); + $response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid])); $response->assertOk(); $response->assertInertia(fn ($page) => $page @@ -40,7 +40,7 @@ test('non-participant cannot view participant detail page in dynamic', function $this->actingAs($outsider); - $response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id])); + $response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid])); $response->assertStatus(403); }); @@ -82,7 +82,7 @@ test('participant detail page displays their recent mutations in dynamic', funct $this->actingAs($owner); - $response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id])); + $response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid])); $response->assertOk(); $response->assertInertia(fn ($page) => $page diff --git a/tests/Feature/ParticipantTest.php b/tests/Feature/ParticipantTest.php index 8cf9adf..b862375 100644 --- a/tests/Feature/ParticipantTest.php +++ b/tests/Feature/ParticipantTest.php @@ -29,7 +29,7 @@ test('participant can update their display name', function () { $this->actingAs($user); - $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + $response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [ 'display_name' => 'Ally', ]); @@ -53,7 +53,7 @@ test('display name update requires display_name parameter', function () { $this->actingAs($user); - $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + $response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [ 'display_name' => '', ]); @@ -68,7 +68,7 @@ test('non participant cannot update display name', function () { $this->actingAs($nonParticipant); - $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + $response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [ 'display_name' => 'Bobby', ]); diff --git a/tests/Feature/PredefinedMutationTest.php b/tests/Feature/PredefinedMutationTest.php index 2a23ce3..8747c55 100644 --- a/tests/Feature/PredefinedMutationTest.php +++ b/tests/Feature/PredefinedMutationTest.php @@ -5,14 +5,13 @@ use App\Models\Dynamic; use App\Models\Ledger; use App\Models\PredefinedMutation; -test('owner can view predefined mutations for ledger', function () { +test('owner can view predefined mutations for dynamic', function () { $owner = User::factory()->create(); $dynamic = Dynamic::factory()->create(); $dynamic->participants()->attach($owner->id, ['role' => 'owner']); - $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]); $predefined = PredefinedMutation::create([ - 'ledger_id' => $ledger->id, + 'dynamic_id' => $dynamic->id, 'name' => 'Weekly Room Cleaning', 'description' => 'Cleaned up the master bedroom', 'amount' => 20, @@ -20,73 +19,71 @@ test('owner can view predefined mutations for ledger', function () { $this->actingAs($owner); - $response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id])); + $response = $this->get(route('dynamics.predefined-mutations.index', $dynamic->uuid)); $response->assertOk(); $response->assertInertia(fn ($page) => $page - ->component('Ledgers/PredefinedMutations/Index') - ->where('ledger.id', $ledger->id) + ->component('Dynamics/PredefinedMutations/Index') ->has('predefined_mutations', 1) ->where('predefined_mutations.0.name', 'Weekly Room Cleaning') ); }); -test('non-owner cannot view predefined mutations for ledger', function () { +test('non-owner cannot view predefined mutations for dynamic', function () { $owner = User::factory()->create(); $participant = User::factory()->create(); $dynamic = Dynamic::factory()->create(); $dynamic->participants()->attach($owner->id, ['role' => 'owner']); $dynamic->participants()->attach($participant->id, ['role' => 'participant']); - $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]); $this->actingAs($participant); - $response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id])); + $response = $this->get(route('dynamics.predefined-mutations.index', $dynamic->uuid)); $response->assertStatus(403); }); -test('owner can create predefined mutations for ledger', function () { +test('owner can create predefined mutations for dynamic', function () { $owner = User::factory()->create(); $dynamic = Dynamic::factory()->create(); $dynamic->participants()->attach($owner->id, ['role' => 'owner']); - $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]); $this->actingAs($owner); - $response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [ + $response = $this->post(route('dynamics.predefined-mutations.store', $dynamic->uuid), [ 'name' => 'Polished mirrors', 'description' => 'Mirror polishing in dungeon', 'amount' => 15, + 'type' => 'reward', ]); $response->assertRedirect(); $this->assertDatabaseHas('predefined_mutations', [ - 'ledger_id' => $ledger->id, + 'dynamic_id' => $dynamic->id, 'name' => 'Polished mirrors', 'amount' => 15, ]); }); -test('non-owner cannot create predefined mutations for ledger', function () { +test('non-owner cannot create predefined mutations for dynamic', function () { $owner = User::factory()->create(); $participant = User::factory()->create(); $dynamic = Dynamic::factory()->create(); $dynamic->participants()->attach($owner->id, ['role' => 'owner']); $dynamic->participants()->attach($participant->id, ['role' => 'participant']); - $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]); $this->actingAs($participant); - $response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [ + $response = $this->post(route('dynamics.predefined-mutations.store', $dynamic->uuid), [ 'name' => 'Polished mirrors', 'description' => 'Mirror polishing in dungeon', 'amount' => 15, + 'type' => 'reward', ]); $response->assertStatus(403); $this->assertDatabaseMissing('predefined_mutations', [ - 'ledger_id' => $ledger->id, + 'dynamic_id' => $dynamic->id, 'name' => 'Polished mirrors', ]); });