From 82d2e5e0de429378ec7597723615a9133d0ce3cf Mon Sep 17 00:00:00 2001 From: Kofi Date: Mon, 29 Sep 2025 20:56:04 +0100 Subject: [PATCH 1/3] fix: Return group with transactions --- app/Models/Transaction.php | 12 ++++++++++++ .../2024_07_20_125117_create_transactions_table.php | 2 ++ tests/Feature/TransactionsTest.php | 9 +++++++++ 3 files changed, 23 insertions(+) diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 9012b65..a327e08 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -61,6 +61,7 @@ class Transaction extends Model 'type', 'party_id', 'wallet_id', + 'group_id', 'user_id', 'transfer_id', 'updated_at', @@ -70,6 +71,7 @@ class Transaction extends Model protected $appends = [ 'wallet', 'party', + 'group', 'categories', 'last_synced_at', 'client_generated_id', @@ -119,6 +121,16 @@ public function party() return $this->belongsTo(Party::class); } + public function getGroupAttribute() + { + return $this->group()->first(); + } + + public function group() + { + return $this->belongsTo(Group::class); + } + public function user() { return $this->belongsTo(User::class); diff --git a/database/migrations/2024_07_20_125117_create_transactions_table.php b/database/migrations/2024_07_20_125117_create_transactions_table.php index 730e3da..d2046f6 100644 --- a/database/migrations/2024_07_20_125117_create_transactions_table.php +++ b/database/migrations/2024_07_20_125117_create_transactions_table.php @@ -19,6 +19,7 @@ public function up(): void $table->text('description')->nullable(); $table->unsignedBigInteger('wallet_id')->nullable(); $table->unsignedBigInteger('party_id')->nullable(); + $table->unsignedBigInteger('group_id')->nullable(); $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('transfer_id')->nullable(); $table->timestamps(); @@ -27,6 +28,7 @@ public function up(): void $table->foreign('transfer_id')->references('id')->on('transfers')->onDelete('cascade'); $table->foreign('wallet_id')->references('id')->on('wallets')->onDelete('cascade'); $table->foreign('party_id')->references('id')->on('parties')->onDelete('cascade'); + $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); } diff --git a/tests/Feature/TransactionsTest.php b/tests/Feature/TransactionsTest.php index 297fcd3..dc9bbc5 100644 --- a/tests/Feature/TransactionsTest.php +++ b/tests/Feature/TransactionsTest.php @@ -18,6 +18,8 @@ class TransactionsTest extends TestCase private $party; + private $group; + private $user; public function test_api_user_can_create_transactions() @@ -37,6 +39,7 @@ private function createTransaction(string $type, array $recurrentData = []): arr 'description' => 'Test transaction description', 'wallet_id' => $this->wallet->id, 'party_id' => $this->party->id, + 'group_id' => $this->group->id, 'datetime' => '2025-04-30T15:17:54.120Z', ]; @@ -58,6 +61,7 @@ private function createTransaction(string $type, array $recurrentData = []): arr 'description', 'datetime', 'party_id', + 'group_id', 'wallet_id', 'user_id', 'client_generated_id', @@ -811,6 +815,11 @@ protected function setUp(): void 'name' => 'Party', 'type' => 'personal', ]); + + $this->group = User::factory()->create()->groups()->create([ + 'name' => 'Group', + 'type' => 'personal', + ]); $this->user = User::factory()->create(); } } From a8e3fae6af8274f46f375189174d7aa7a964c28e Mon Sep 17 00:00:00 2001 From: Kofi Date: Mon, 29 Sep 2025 20:57:25 +0100 Subject: [PATCH 2/3] chore: Return items greater than synced_since --- app/Http/Traits/ApiQueryable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Traits/ApiQueryable.php b/app/Http/Traits/ApiQueryable.php index 6178e10..f6cd8f8 100644 --- a/app/Http/Traits/ApiQueryable.php +++ b/app/Http/Traits/ApiQueryable.php @@ -22,7 +22,7 @@ protected function applyApiQuery(Request $request, Builder|Relation $query): arr if ($request->has('synced_since')) { try { $date = Carbon::parse($request->synced_since); - $query = $query->where('updated_at', '>=', $date)->withTrashed(); + $query = $query->where('updated_at', '>', $date)->withTrashed(); } catch (\Exception $exception) { throw new \InvalidArgumentException('Invalid date format for synced_since parameter.'); } From abbd36f9760398b2249282599ea1834beb038425 Mon Sep 17 00:00:00 2001 From: Kofi Date: Sat, 4 Oct 2025 12:25:32 +0100 Subject: [PATCH 3/3] fix: Add multiple groups to transactions --- .../API/v1/TransactionController.php | 11 ++++++- app/Models/Transaction.php | 14 ++------ app/Traits/Groupable.php | 24 ++++++++++++++ ...07_20_125117_create_transactions_table.php | 2 -- .../2024_07_20_131629_create_groupables.php | 1 + public/docs/api.json | 13 ++++++-- tests/Feature/TransactionsTest.php | 32 ++++++++++++------- 7 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 app/Traits/Groupable.php diff --git a/app/Http/Controllers/API/v1/TransactionController.php b/app/Http/Controllers/API/v1/TransactionController.php index 13180f0..94c2a04 100644 --- a/app/Http/Controllers/API/v1/TransactionController.php +++ b/app/Http/Controllers/API/v1/TransactionController.php @@ -268,6 +268,10 @@ public function store(Request $request): JsonResponse $transaction->categories()->sync($categories); } + if (isset($data['group_id'])) { + $transaction->groups()->sync($data['group_id']); + } + if ($request->hasFile('files')) { foreach ($request->file('files') as $file) { $path = $file->store('transactions'); @@ -417,13 +421,14 @@ public function show($id, Request $request): JsonResponse new OA\Property(property: 'type', type: 'string', enum: ['income', 'expense']), new OA\Property(property: 'date', type: 'string', format: 'date'), new OA\Property(property: 'party_id', type: 'integer'), + new OA\Property(property: 'group_id', type: 'integer'), new OA\Property(property: 'description', type: 'string'), new OA\Property(property: 'wallet_id', type: 'integer'), - new OA\Property(property: 'group_id', type: 'integer'), new OA\Property(property: 'is_recurring', description: 'Set the transaction as a recurring transaction', type: 'boolean'), new OA\Property(property: 'recurrence_period', description: 'Set how often the transaction should repeat', type: 'string'), new OA\Property(property: 'recurrence_interval', description: 'Set how often the transaction should repeat', type: 'integer'), new OA\Property(property: 'recurrence_ends_at', description: 'When the transaction stops repeating', type: 'date-time'), + new OA\Property(property: 'categories', type: 'array', items: new OA\Items(description: 'Category ID array', type: 'integer')), new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'), ] ) @@ -541,6 +546,10 @@ public function update(Request $request, $id): JsonResponse $transaction->categories()->sync($validatedData['categories']); } + if (isset($validatedData['group_id'])) { + $transaction->groups()->sync([$validatedData['group_id']]); + } + $user = $request->user(); if (isset($request['client_id']) && ! $transaction->client_id) { $transaction->setClientGeneratedId($request['client_id'], $user); diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index a327e08..2672c56 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\Groupable; use App\Traits\HasClientCreatedAt; use App\Traits\Syncable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -47,7 +48,7 @@ )] class Transaction extends Model { - use HasClientCreatedAt, HasFactory, SoftDeletes, Syncable; + use Groupable, HasClientCreatedAt, HasFactory, SoftDeletes, Syncable; /** * The attributes that are mass assignable. @@ -61,7 +62,6 @@ class Transaction extends Model 'type', 'party_id', 'wallet_id', - 'group_id', 'user_id', 'transfer_id', 'updated_at', @@ -121,16 +121,6 @@ public function party() return $this->belongsTo(Party::class); } - public function getGroupAttribute() - { - return $this->group()->first(); - } - - public function group() - { - return $this->belongsTo(Group::class); - } - public function user() { return $this->belongsTo(User::class); diff --git a/app/Traits/Groupable.php b/app/Traits/Groupable.php new file mode 100644 index 0000000..4283762 --- /dev/null +++ b/app/Traits/Groupable.php @@ -0,0 +1,24 @@ +groups()->get(); + } + + public function getGroupAttribute() + { + return $this->groups()->first(); + } + + public function groups(): MorphToMany + { + return $this->morphToMany(Group::class, 'groupable'); + } +} diff --git a/database/migrations/2024_07_20_125117_create_transactions_table.php b/database/migrations/2024_07_20_125117_create_transactions_table.php index d2046f6..730e3da 100644 --- a/database/migrations/2024_07_20_125117_create_transactions_table.php +++ b/database/migrations/2024_07_20_125117_create_transactions_table.php @@ -19,7 +19,6 @@ public function up(): void $table->text('description')->nullable(); $table->unsignedBigInteger('wallet_id')->nullable(); $table->unsignedBigInteger('party_id')->nullable(); - $table->unsignedBigInteger('group_id')->nullable(); $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('transfer_id')->nullable(); $table->timestamps(); @@ -28,7 +27,6 @@ public function up(): void $table->foreign('transfer_id')->references('id')->on('transfers')->onDelete('cascade'); $table->foreign('wallet_id')->references('id')->on('wallets')->onDelete('cascade'); $table->foreign('party_id')->references('id')->on('parties')->onDelete('cascade'); - $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); } diff --git a/database/migrations/2024_07_20_131629_create_groupables.php b/database/migrations/2024_07_20_131629_create_groupables.php index 430c825..34184d9 100644 --- a/database/migrations/2024_07_20_131629_create_groupables.php +++ b/database/migrations/2024_07_20_131629_create_groupables.php @@ -13,6 +13,7 @@ public function up(): void { Schema::create('groupables', function (Blueprint $table) { $table->id(); + $table->unsignedBigInteger('group_id'); $table->unsignedBigInteger('groupable_id'); $table->string('groupable_type'); $table->timestamps(); diff --git a/public/docs/api.json b/public/docs/api.json index d4d3a8d..ea01954 100644 --- a/public/docs/api.json +++ b/public/docs/api.json @@ -1359,15 +1359,15 @@ "party_id": { "type": "integer" }, + "group_id": { + "type": "integer" + }, "description": { "type": "string" }, "wallet_id": { "type": "integer" }, - "group_id": { - "type": "integer" - }, "is_recurring": { "description": "Set the transaction as a recurring transaction", "type": "boolean" @@ -1384,6 +1384,13 @@ "description": "When the transaction stops repeating", "type": "date-time" }, + "categories": { + "type": "array", + "items": { + "description": "Category ID array", + "type": "integer" + } + }, "updated_at": { "type": "string", "format": "date-time" diff --git a/tests/Feature/TransactionsTest.php b/tests/Feature/TransactionsTest.php index dc9bbc5..f4cb90a 100644 --- a/tests/Feature/TransactionsTest.php +++ b/tests/Feature/TransactionsTest.php @@ -20,6 +20,8 @@ class TransactionsTest extends TestCase private $group; + private $category; + private $user; public function test_api_user_can_create_transactions() @@ -40,6 +42,7 @@ private function createTransaction(string $type, array $recurrentData = []): arr 'wallet_id' => $this->wallet->id, 'party_id' => $this->party->id, 'group_id' => $this->group->id, + 'categories' => [$this->category->id], 'datetime' => '2025-04-30T15:17:54.120Z', ]; @@ -61,7 +64,8 @@ private function createTransaction(string $type, array $recurrentData = []): arr 'description', 'datetime', 'party_id', - 'group_id', + 'group', + 'categories', 'wallet_id', 'user_id', 'client_generated_id', @@ -444,6 +448,16 @@ public function test_next_scheduled_transaction_should_not_run_if_recurrence_cha $this->assertEquals(2, $transactions); } + private function runQueueWorkerOnce(): void + { + Artisan::call('queue:work', [ + 'connection' => 'database', + '--once' => true, + '--sleep' => 0, + '--tries' => 1, + ]); + } + public function test_recurring_transaction_runs_on_next_scheduled_date() { // Test weekly recurrence @@ -464,16 +478,6 @@ public function test_recurring_transaction_runs_on_next_scheduled_date() $this->assertEquals(2, $transactions); } - private function runQueueWorkerOnce(): void - { - Artisan::call('queue:work', [ - 'connection' => 'database', - '--once' => true, - '--sleep' => 0, - '--tries' => 1, - ]); - } - public function test_api_user_can_create_recurring_transactions_with_different_periods() { // Test weekly recurrence @@ -820,6 +824,12 @@ protected function setUp(): void 'name' => 'Group', 'type' => 'personal', ]); + + $this->category = User::factory()->create()->categories()->create([ + 'name' => 'Category', + 'type' => 'income', + ]); + $this->user = User::factory()->create(); } }