Skip to content

enh : Improve CRUD implementation for transactions #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 26, 2025
Merged

Conversation

nfebe
Copy link
Contributor

@nfebe nfebe commented Jan 25, 2025

Summary by CodeRabbit

Release Notes

New Features

  • Enhanced transaction management with a centralized Transaction model.
  • Added support for transaction categories.
  • Introduced default user groups upon registration.
  • Improved API documentation with OpenAPI annotations.
  • Added new factories for Category, Group, Party, and Wallet models.

Improvements

  • Simplified transaction creation and retrieval.
  • Added flexible filtering for transactions.
  • Enhanced user and transaction relationships.
  • Improved database schema for groups, parties, and wallets.
  • Updated API to include bearer authentication.

Bug Fixes

  • Resolved issues with income and expense tracking.
  • Improved validation for transaction creation.

Breaking Changes

  • Removed separate Income and Expense models.
  • Updated transaction creation requirements.
  • Modified API endpoint structures.
  • Enhanced structure of the groups, parties, and wallets tables.

@nfebe nfebe marked this pull request as draft January 25, 2025 19:57
@trakli trakli deleted a comment from coderabbitai bot Jan 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
app/Http/Controllers/API/v1/TransactionController.php (1)

27-33: Consider adjusting the required setting.
This parameter is assigned a default value (20), yet it's also marked as required. If you truly intend to always require a user-supplied limit, consider removing the default to ensure clients specify it. Otherwise, mark it as optional.

 new OA\Parameter(
     name: 'limit',
     in: 'query',
     description: 'Number of transactions to fetch',
-    required: true,
+    required: false,
     schema: new OA\Schema(type: 'integer', default: 20)
 ),
database/migrations/2024_07_20_125016_create_transactions_table.php (2)

17-17: Switched to datetime.
Using a datetime column is flexible for date/time support. Confirm that references and validations across the app use the same format.


18-18: Use of enum for 'type'.
This is fine for strictly defining 'income' or 'expense'. If you anticipate new transaction types, consider a string or separate referencing table for easier extension.

app/Http/Controllers/API/ApiController.php (1)

26-35: Trailing comma style violation noted.
Your pipeline highlights a trailing comma in this multiline array. Remove it to pass style checks.

 #[OA\Components(
     securitySchemes: [
         new OA\SecurityScheme(
             securityScheme: 'bearerAuth',
             type: 'http',
             scheme: 'bearer',
             description: 'Bearer token authentication'
-        ),
+        )
     ]
 )]
🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: trailing comma in multiline

public/docs/api.json (1)

927-936: Consider making the limit parameter optional.

Since the parameter has a default value of 20, it should be optional rather than required. This allows clients to use the default pagination without explicitly providing a limit.

Apply this diff to make the limit parameter optional:

                    {
                        "name": "limit",
                        "in": "query",
                        "description": "Number of transactions to fetch",
-                        "required": true,
+                        "required": false,
                        "schema": {
                            "type": "integer",
                            "default": 20
                        }
                    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d772ae and c7e6d3a.

📒 Files selected for processing (9)
  • app/Http/Controllers/API/ApiController.php (2 hunks)
  • app/Http/Controllers/API/v1/Auth/AuthController.php (2 hunks)
  • app/Http/Controllers/API/v1/Auth/PasswordResetController.php (2 hunks)
  • app/Http/Controllers/API/v1/TransactionController.php (5 hunks)
  • app/Models/Expense.php (0 hunks)
  • app/Models/Income.php (0 hunks)
  • database/migrations/2024_07_20_125016_create_transactions_table.php (1 hunks)
  • database/migrations/2024_07_20_125022_create_expenses_table.php (0 hunks)
  • public/docs/api.json (8 hunks)
💤 Files with no reviewable changes (3)
  • database/migrations/2024_07_20_125022_create_expenses_table.php
  • app/Models/Expense.php
  • app/Models/Income.php
🧰 Additional context used
🪛 GitHub Actions: Checks
app/Http/Controllers/API/ApiController.php

[error] Style violation: trailing comma in multiline

app/Http/Controllers/API/v1/Auth/PasswordResetController.php

[error] Style violation: unary operator spaces

app/Http/Controllers/API/v1/Auth/AuthController.php

[error] Style violation: unary operator spaces

🔇 Additional comments (22)
app/Http/Controllers/API/v1/TransactionController.php (9)

6-6: Nice model reference.
You're now correctly importing the unified Transaction model, which aligns with the updated data structure.


50-50: Query default looks good.
Using $request->query('limit', 20) is consistent with the default logic.


56-56: Pagination usage is appropriate.
This is correct for retrieving a paginated list of transactions. Keep an eye on performance for larger datasets.


71-71: Good inclusion of 'datetime' property.
This clarifies the expected format, but see adjacent comment about the mismatch in the required list.


95-95: Validation looks correct.
Requiring a numeric amount is sensible.


98-98: Confirm the 'datetime' validation rule.
Laravel does not include a built-in 'datetime' validation. Consider 'date' or 'date_format' to ensure correct validation.


100-100: Optional party association.
Marking party_id as nullable is flexible for transactions that have no party involvement.


101-101: Correct association requirement.
Making wallet_id mandatory is consistent with typical wallet-based transaction logic.


109-109: Confirm mass assignment protection.
Ensure the Transaction model has $fillable or $guarded set appropriately to avoid mass assignment vulnerabilities.

database/migrations/2024_07_20_125016_create_transactions_table.php (2)

14-14: Unified table creation.
Renaming/creating transactions aligns with consolidating incomes and expenses. Make sure you migrate the existing data properly.


20-20: Nullable wallet_id.
This is helpful for transactions that might not be wallet-based. Ensure your controller logic handles nullable wallets properly.

app/Http/Controllers/API/ApiController.php (1)

9-13: Global security annotation is a good addition.
This clarifies the bearer authentication requirement for all endpoints unless otherwise overridden.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: trailing comma in multiline

app/Http/Controllers/API/v1/Auth/AuthController.php (3)

16-16: LGTM! OpenAPI tag annotation is well-placed and descriptive.

The tag annotation correctly categorizes the authentication endpoints and provides a clear description.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces


22-22: LGTM! Security annotation correctly indicates no authentication required.

The security: [] annotation is appropriate for the registration endpoint as new users should not need to authenticate.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces


75-75: LGTM! Security annotation correctly indicates no authentication required.

The security: [] annotation is appropriate for the login endpoint as users need to authenticate through this endpoint.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces

app/Http/Controllers/API/v1/Auth/PasswordResetController.php (3)

15-15: LGTM! OpenAPI tag annotation maintains consistency with AuthController.

The tag annotation correctly categorizes the password reset endpoints under the same 'Authentication' tag.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces


23-23: LGTM! Security annotation correctly indicates no authentication required.

The security: [] annotation is appropriate for the password reset code endpoint as users who forgot their password cannot authenticate.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces


90-90: LGTM! Security annotation correctly indicates no authentication required.

The security: [] annotation is appropriate for the password reset endpoint as users who are resetting their password cannot authenticate.

🧰 Tools
🪛 GitHub Actions: Checks

[error] Style violation: unary operator spaces

public/docs/api.json (4)

73-74: LGTM! Security annotations align with controller changes.

The "security": [] annotations are consistently applied to all authentication-related endpoints, matching the controller configurations.

Also applies to: 123-124, 174-175, 238-239


973-973: LGTM! Required fields are appropriately updated.

The required fields for transaction creation have been correctly updated to reflect the essential attributes while making group_id optional.


1542-1547: LGTM! Security scheme is well-defined.

The bearer token authentication scheme is correctly configured according to OpenAPI specification.


1550-1554: LGTM! Global security requirement ensures secure-by-default behavior.

The global security requirement correctly enforces bearer token authentication for all endpoints by default, while allowing specific endpoints to opt-out using "security": [].

Copy link

coderabbitai bot commented Jan 25, 2025

Walkthrough

The pull request introduces a comprehensive refactoring of the transaction management system, consolidating income and expense tracking into a single Transaction model. Key changes include centralizing transaction logic, enhancing API documentation with OpenAPI attributes, and introducing new factories and migrations to support the updated data model. The changes streamline the application's financial tracking capabilities by removing separate income and expense models, adding more flexible categorization, and improving overall system architecture.

Changes

File/Directory Change Summary
app/Http/Controllers/API/ApiController.php Added OpenAPI security attributes and utility methods for resource access and request validation.
app/Http/Controllers/API/v1/Auth/* Enhanced OpenAPI documentation for authentication endpoints.
app/Http/Controllers/API/v1/TransactionController.php Refactored transaction management to use a single Transaction model.
app/Models/ Removed Expense and Income models, updated Transaction model with new properties and relationships.
app/Models/Group.php Introduced Sluggable trait for automatic slug generation.
app/Models/Category.php Added constants for category types.
database/factories/ Added factories for Category, Group, Party, User, Wallet with realistic data generation.
database/migrations/ Updated table schemas for groups, parties, wallets, categories, and added new transactions table.
tests/ Added comprehensive TransactionsTest and updated TestCase for more efficient test setup.

Sequence Diagram

sequenceDiagram
    participant User
    participant AuthController
    participant TransactionController
    participant Transaction
    participant Category
    participant Wallet
    participant Party

    User->>AuthController: Register/Login
    AuthController-->>User: Authentication Token
    User->>TransactionController: Create Transaction
    TransactionController->>Transaction: Create New Transaction
    TransactionController->>Category: Associate Categories
    TransactionController->>Wallet: Update Wallet Balance
    TransactionController->>Party: Link Transaction
    TransactionController-->>User: Transaction Created
Loading

Poem

🐰 Hop, hop, hooray! A transaction tale today!
From Income and Expense, we've cleared the way
One model to rule them, categories to play
With wallets and parties in financial ballet
CodeRabbit's magic makes tracking a breeze! 🌟


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Signed-off-by: nfebe <fenn25.fn@gmail.com>
@nfebe nfebe marked this pull request as ready for review January 26, 2025 01:02
@nfebe nfebe requested a review from kofimokome January 26, 2025 01:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🔭 Outside diff range comments (1)
app/Models/Group.php (1)

Line range hint 10-20: Update OpenAPI schema to include the slug property.

The OpenAPI schema is missing the slug property which is now a fillable attribute in the model.

Add the slug property to the schema:

 #[OA\Schema(
     schema: 'Group',
     type: 'object',
     properties: [
         new OA\Property(property: 'id', type: 'integer', description: 'ID of the group'),
         new OA\Property(property: 'name', type: 'string', description: 'Name of the group'),
         new OA\Property(property: 'description', type: 'string', description: 'Description of the group'),
+        new OA\Property(property: 'slug', type: 'string', description: 'URL-friendly version of the name'),
     ]
 )]
🧹 Nitpick comments (17)
database/factories/UserFactory.php (1)

33-33: Consider using more secure test passwords.

While password123 is fine for testing, consider:

  1. Using environment variables for test passwords
  2. Implementing a more robust password generation strategy for testing different password complexity scenarios

Example implementation:

-            'password' => static::$password ??= Hash::make('password123'),
+            'password' => static::$password ??= Hash::make(env('TEST_USER_PASSWORD', 'password123')),

You could also create a helper method for generating test passwords with different complexity levels:

protected function generateTestPassword(): string
{
    return env('TEST_USER_PASSWORD') ?? Str::random(12) . '!2A';  // Ensures mixed case, numbers, and symbols
}
app/Models/Transaction.php (3)

18-24: Use ‘date-time’ format instead of ‘date’
OpenAPI typically uses "date-time" to represent date and time in strings. Suggest changing format: 'date' to format: 'date-time' for better clarity and alignment with the standard.

- new OA\Property(property: 'datetime', type: 'string', format: 'date', description: 'Date and time of the transaction'),
+ new OA\Property(property: 'datetime', type: 'string', format: 'date-time', description: 'Date and time of the transaction'),

46-46: Potential performance overhead with $appends
Appending relationships (wallet, party, categories) by default can trigger multiple queries for every transaction retrieval. Consider removing them from $appends if you’d rather load them on-demand or use eager loading to reduce N+1 queries.


48-51: Avoid overshadowing relationship names with accessors
All three accessors (getCategoriesAttribute, getWalletAttribute, getPartyAttribute) mirror the same relationship names. This can be confusing and may lead to unexpected outcomes when calling $model->categories vs. $model->categories(). Renaming either the accessors or the relationship methods can improve clarity.

Also applies to: 53-56, 58-61

app/Http/Controllers/API/v1/TransactionController.php (3)

27-33: Limit parameter should be optional
Currently marked as required: true, yet a default of 20 is provided. Generally, limit parameters are optional. Updating to required: false may be more consistent with typical API conventions.

- required: true,
+ required: false,

96-96: Validation for ‘datetime’
Currently set to nullable|date. If you intend to store both date and time, you might want date_format:Y-m-d H:i:s or a more precise check.

- 'datetime' => 'nullable|date',
+ 'datetime' => 'nullable|date_format:Y-m-d H:i:s',

Also applies to: 99-104


111-117: Shadowing of $request variable
Reusing $request to store validated data can be confusing. Prefer naming it $validatedData to avoid overshadowing the original $request object.

-$request = $validator->validated();
+$validatedData = $validator->validated();

...

-$transaction = Transaction::create($request);
+$transaction = Transaction::create($validatedData);
tests/TestCase.php (1)

10-16: Fresh seeding for each test run
migrate:fresh --seed ensures a clean database state but can slow large test suites. Consider using transactions or rolling back changes as an alternative for faster tests.

database/migrations/2024_07_20_125030_create_groups_table.php (1)

18-18: Consider using an enum for the type column.

Using a string type without constraints allows any value. Consider using an enum if you have a predefined set of group types to ensure data consistency.

-            $table->string('type')->nullable();
+            $table->enum('type', ['type1', 'type2', 'type3'])->nullable();

Please replace 'type1', 'type2', 'type3' with your actual group types.

database/migrations/2024_07_20_125116_create_wallets_table.php (1)

17-17: Consider reordering enum values logically.

The enum values could be ordered more logically, grouping similar payment methods together (e.g., electronic methods vs physical methods).

-            $table->enum('type', ['bank', 'cash', 'credit_card', 'mobile']);
+            $table->enum('type', ['bank', 'credit_card', 'mobile', 'cash']);
database/seeders/UserSeeder.php (1)

22-36: Consider using createMany() for better performance.

Instead of multiple create() calls in a loop, consider using createMany() for better performance when creating multiple related entities.

-            Party::factory(5)->create([
-                'user_id' => $user->id,
-            ]);
+            Party::factory()->count(5)->createMany(
+                array_fill(0, 5, ['user_id' => $user->id])
+            );

Similar changes can be applied to Category, Wallet, and Group creation.

database/factories/PartyFactory.php (1)

20-31: Consider separating party types using factory states.

The current implementation mixes different types of parties (individuals and institutions) in a single list. Consider using factory states to generate different types of parties.

 class PartyFactory extends Factory
 {
     public function definition(): array
     {
         return [
-            'name' => $this->faker->randomElement([
-                'John Doe',
-                'University of Buea',
-                'Jane Doe',
-                'Bob',
-                'University of Cambridge',
-                'Harvard University',
-                'Alice',
-                'Filling Station',
-                'Wallibi Park',
-                'Disneyland',
-            ]),
+            'name' => $this->faker->name,
             'description' => $this->faker->sentence,
         ];
     }
+
+    public function institution(): static
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'name' => $this->faker->company,
+            ];
+        });
+    }
+
+    public function university(): static
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'name' => $this->faker->randomElement([
+                    'University of Buea',
+                    'University of Cambridge',
+                    'Harvard University',
+                ]),
+            ];
+        });
+    }
 }
database/factories/CategoryFactory.php (1)

20-31: Consider implementing hierarchical categories.

The current flat structure of categories might be limiting. Consider implementing a parent-child relationship for more flexible categorization.

Example implementation:

 // In database/migrations/create_categories_table.php
 Schema::create('categories', function (Blueprint $table) {
     // ... existing columns
+    $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
 });

 // In CategoryFactory.php
     public function definition(): array
     {
         return [
             'name' => $this->faker->randomElement([
                 'Groceries',
                 'Rent',
                 // ... other categories
             ]),
+            'parent_id' => null,
             'description' => $this->faker->sentence,
             'type' => $this->faker->randomElement(['income', 'expense']),
         ];
     }
+
+    public function asSubcategory(): static
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'parent_id' => Category::factory(),
+            ];
+        });
+    }
app/Http/Controllers/API/ApiController.php (1)

Line range hint 19-24: Address the TODO comment regarding server variables.

The commented-out server variables configuration needs to be resolved.

Would you like me to help implement a solution for the server variables configuration that doesn't cause an error?

public/docs/api.json (3)

898-905: Consider making the limit parameter optional.

The limit parameter for pagination is a good addition, but marking it as required might be too restrictive. Consider making it optional while keeping the default value of 20.

-                        "required": true,
+                        "required": false,

970-976: Enhance documentation for categories field.

Consider adding examples to better document the categories field usage.

                                    "categories": {
                                        "type": "array",
                                        "items": {
                                            "description": "Category ID array",
-                                            "type": "integer"
+                                            "type": "integer",
+                                            "example": 1
                                        },
+                                        "example": [1, 2, 3]
                                    }

Also applies to: 1495-1501


1538-1541: Improve tag descriptions.

Some tag descriptions are too brief or just repeat the tag name. Consider adding more descriptive explanations.

        {
            "name": "Groups",
-            "description": "Groups"
+            "description": "Endpoints for managing transaction groups"
        },
        {
            "name": "Party",
-            "description": "Party"
+            "description": "Endpoints for managing transaction parties"
        },
        {
            "name": "Wallet",
-            "description": "Wallet"
+            "description": "Endpoints for managing transaction wallets"
        }

Also applies to: 1547-1558

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aef133 and fc3314c.

📒 Files selected for processing (24)
  • app/Http/Controllers/API/ApiController.php (2 hunks)
  • app/Http/Controllers/API/v1/Auth/AuthController.php (2 hunks)
  • app/Http/Controllers/API/v1/Auth/PasswordResetController.php (2 hunks)
  • app/Http/Controllers/API/v1/TransactionController.php (8 hunks)
  • app/Listeners/UserRegistered.php (1 hunks)
  • app/Models/Expense.php (0 hunks)
  • app/Models/Group.php (2 hunks)
  • app/Models/Income.php (0 hunks)
  • app/Models/Transaction.php (2 hunks)
  • database/factories/CategoryFactory.php (1 hunks)
  • database/factories/GroupFactory.php (1 hunks)
  • database/factories/PartyFactory.php (1 hunks)
  • database/factories/UserFactory.php (2 hunks)
  • database/factories/WalletFactory.php (1 hunks)
  • database/migrations/2024_07_20_125016_create_transactions_table.php (1 hunks)
  • database/migrations/2024_07_20_125030_create_groups_table.php (1 hunks)
  • database/migrations/2024_07_20_125104_create_parties_table.php (1 hunks)
  • database/migrations/2024_07_20_125116_create_wallets_table.php (1 hunks)
  • database/migrations/2024_07_20_125455_create_categories_table.php (1 hunks)
  • database/migrations/2025_01_25_232422_create_categorizables.php (2 hunks)
  • database/seeders/UserSeeder.php (1 hunks)
  • public/docs/api.json (8 hunks)
  • tests/Feature/TransactionsTest.php (1 hunks)
  • tests/TestCase.php (1 hunks)
💤 Files with no reviewable changes (2)
  • app/Models/Expense.php
  • app/Models/Income.php
🔇 Additional comments (23)
database/factories/UserFactory.php (2)

17-17: LGTM! Explicit null initialization improves clarity.

The explicit null initialization of the password property follows good practices and makes the code's intent clearer.


Line range hint 17-35: Verify user-transaction relationship integrity.

Since this PR involves transaction management system refactoring, ensure that any user-transaction relationships are properly maintained with these user model changes.

Let's check the transaction model relationships:

✅ Verification successful

UserFactory changes are safe for transaction relationships

The changes only affect test data generation and don't impact the user-transaction relationship integrity. The Transaction model properly maintains its belongsTo relationship with the User model.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check transaction model for user relationships
echo "Checking Transaction model relationships:"
rg -A 5 "class Transaction|function user|belongs" app/Models/Transaction.php || echo "Transaction.php not found"

echo -e "\nChecking for any relationship changes in migrations:"
fd -e php -x grep -l "foreign|references" {} \; database/migrations/

Length of output: 1216


Script:

#!/bin/bash
# Check migrations and tests
echo "Checking migrations for foreign keys:"
fd . database/migrations/ -t f -e php -x grep -l "foreign|references" {}

echo -e "\nChecking tests using UserFactory with transactions:"
rg -l "UserFactory|Transaction::factory" tests/

Length of output: 330

app/Models/Transaction.php (4)

7-7: Import for MorphToMany Relationship
Good addition to properly define the morph-to-many relationship.


63-66: Simple belongsTo relationships
Wallet, party, and user relationships appear straightforward and correct.

Also applies to: 68-71, 73-76


78-81: Morph-to-many relationship to categories
Implementation looks good. Ensure any relevant pivot table migrations are in place for categorizable references.


32-44: Verify handling of ‘group_id’ in mass-assignment
You have added party_id and wallet_id to the fillable array, but not group_id, which also appears elsewhere. Verify if you want it to be mass assignable too.

app/Http/Controllers/API/v1/TransactionController.php (8)

6-6: Import of Transaction model
Good step towards consolidating income/expense logic into a unified model.


56-56: Using paginate on typed transactions
Looks good. This unifies the query logic for income/expense.


119-121: Syncing categories
Approach looks correct for many-to-many associations.


170-170: Filtering by ‘type’ for show
Matches the approach used in destroy. Consistent usage is appreciated.


233-240: Consistent validation for update
Mirroring the store validation is good. Same note about datetime vs. date applies here.


257-259: Category sync on update
Correct approach to maintain many-to-many relationships.


307-307: Consistent type-based query for destroy
Keeps logic aligned with show method.


50-50: 🛠️ Refactor suggestion

Inconsistency with required: true vs. default
You’ve set a default value for $limit, but in OpenAPI it’s required. These conflict. Either remove the default or make the parameter optional.

- required: true,
+ required: false,

Likely invalid or redundant comment.

database/migrations/2024_07_20_125016_create_transactions_table.php (1)

17-17: Consider non-nullable datetime column.

The datetime column being nullable might lead to data integrity issues. Transactions should typically have a timestamp.

-            $table->datetime('datetime')->nullable();
+            $table->datetime('datetime');
database/seeders/UserSeeder.php (1)

17-20: Consider adding unique constraint validation for email and username.

While the current implementation creates predictable test data, it doesn't validate potential unique constraints. Consider adding a try-catch block to handle potential duplicate key violations.

database/factories/WalletFactory.php (1)

31-31: 🛠️ Refactor suggestion

Consider using model constants for wallet types.

Hardcoding type values in the factory could lead to inconsistencies. Consider defining these as constants in the Wallet model and referencing them here.

Example implementation:

 // In app/Models/Wallet.php
 class Wallet extends Model
 {
+    public const TYPE_BANK = 'bank';
+    public const TYPE_CASH = 'cash';
+    public const TYPE_CREDIT_CARD = 'credit_card';
+    public const TYPE_MOBILE = 'mobile';
+
+    public const TYPES = [
+        self::TYPE_BANK,
+        self::TYPE_CASH,
+        self::TYPE_CREDIT_CARD,
+        self::TYPE_MOBILE,
+    ];
 }

 // In WalletFactory.php
-            'type' => $this->faker->randomElement(['bank', 'cash', 'credit_card', 'mobile']),
+            'type' => $this->faker->randomElement(Wallet::TYPES),
app/Models/Group.php (2)

32-44: LGTM! Well-implemented sluggable configuration.

The Sluggable trait is properly implemented with correct configuration to generate slugs from the name field.


46-49: LGTM! Proper relationship definition.

The belongsTo relationship with User is correctly defined.

app/Http/Controllers/API/ApiController.php (1)

Line range hint 9-35: LGTM! Comprehensive OpenAPI security configuration.

The bearer token authentication is properly configured with clear documentation.

app/Http/Controllers/API/v1/Auth/AuthController.php (1)

Line range hint 14-91: LGTM! Well-documented authentication endpoints.

The OpenAPI documentation is comprehensive and correctly specifies:

  • Authentication tag and description
  • Security requirements (none for auth endpoints)
  • Request/response schemas
app/Http/Controllers/API/v1/Auth/PasswordResetController.php (1)

14-14: LGTM! OpenAPI documentation changes are well-structured.

The addition of OpenAPI tags and security configurations correctly documents that these password reset endpoints are publicly accessible (no authentication required).

Also applies to: 22-22, 89-89

public/docs/api.json (1)

74-74: LGTM! Security configurations are well-structured.

The changes properly configure authentication:

  • Public endpoints (/register, /login, password reset) are correctly marked as not requiring authentication
  • Bearer token authentication is properly defined as the default security scheme
  • Global security requirement ensures authenticated access by default

Also applies to: 124-124, 175-175, 239-239, 1524-1529, 1532-1536

Comment on lines +30 to +31
'username' => fake()->unique()->userName(),
'phone' => fake()->phoneNumber(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Phone number validation needs improvement

The review comment is valid. Current implementation has basic validation but could be enhanced:

  • Database: Phone is nullable without uniqueness constraint
  • Validation rules only check for string type and max length
  • No specific phone number format validation

Locations requiring attention:

  • database/migrations/2014_10_12_000000_create_users_table.php: Consider adding unique constraint if phone numbers should be unique
  • app/Http/Controllers/API/v1/Auth/AuthController.php: Consider adding phone format validation using regex or Laravel's phone validation rule
🔗 Analysis chain

Consider adding phone number validation and uniqueness constraints.

While the username field is correctly marked as unique, the phone number field might benefit from:

  1. Format validation to ensure consistent phone number formats
  2. Potentially marking it as unique if phone numbers should be unique per user

Let's check if phone validation exists in the User model:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for phone validation rules in User model and migration
echo "Checking User model for phone validation:"
rg -A 5 "phone" app/Models/User.php || echo "User.php not found"

echo -e "\nChecking User migration for phone constraints:"
fd -e php -x grep -l "phone" {} \; -x cat {} \; database/migrations/

Length of output: 768


Script:

#!/bin/bash
# Search migrations with correct path pattern
echo "Checking migrations for phone column:"
fd . database/migrations/ -e php -x grep -l "phone" {} \; -x cat {} \;

echo -e "\nChecking for Form Request validation rules:"
fd -e php -x grep -A 5 "phone.*=>" {} \; app/Http/Requests/

echo -e "\nChecking for any phone validation rules across codebase:"
rg "phone.*=>" -A 5 app/

Length of output: 2547

@nfebe nfebe force-pushed the fix/transactions branch 2 times, most recently from 2b106a8 to ed7e2aa Compare January 26, 2025 14:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 9

🔭 Outside diff range comments (4)
app/Models/User.php (1)

Line range hint 19-19: Fix typo in fillable array.

There's a typo in the $fillable array: 'lsat_name' should be 'last_name'.

-        'lsat_name',
+        'last_name',
app/Http/Controllers/API/ApiController.php (1)

Fix OpenAPI server variables syntax error

The error occurs because the ServerVariable objects are incorrectly instantiated using new within the attribute. Here's the correct syntax:

#[OA\Server(
    url: '{protocol}://{host}/api/v1',
    description: 'Dynamic server URL',
    variables: [
        new OA\ServerVariable(
            name: 'protocol',
            default: 'https',
            enum: ['http', 'https']
        ),
        new OA\ServerVariable(
            name: 'host',
            default: 'api.trakli.io',
            enum: ['api.trakli.io', 'api.staging.example.com']
        )
    ]
)]

The variables property should be an array of ServerVariable objects without using the new keyword directly in the attribute definition.

🔗 Analysis chain

Line range hint 22-28: Investigate and fix the server variables error.

The TODO comment indicates an issue with server variables. This should be investigated to ensure proper dynamic server URL configuration.

Let's verify the OpenAPI specification for server variables:

Would you like me to investigate the error and propose a solution?

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check OpenAPI specification for server variables syntax
curl -s https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/versions/3.0.0.md | grep -A 10 "##### Server Variable Object"

Length of output: 136


Script:

#!/bin/bash
# Search for OpenAPI server annotations and their implementations
ast-grep --pattern 'new OA\Server'

# Search for other OpenAPI variable definitions
ast-grep --pattern 'new OA\ServerVariable'

# Check composer.json for OpenAPI library version
cat composer.json

# Search for any error messages related to ServerVariable
rg -i "servervariable.*error" -A 2 -B 2

Length of output: 68197


Script:

#!/bin/bash
# Search for OpenAPI server configurations in PHP files
rg -t php "@OA\\Server" -B 2 -A 2

# Search for any OpenAPI server variable examples or documentation
fd -e php -e md . | xargs rg -l "ServerVariable"

# Check for any OpenAPI generation errors in logs
fd -e log . | xargs rg -i "openapi|swagger" -A 2 -B 2

# Look for example OpenAPI configurations
fd README -e md | xargs rg -i "openapi|swagger" -A 2 -B 2

Length of output: 66193


Script:

#!/bin/bash
# Get the full content of ApiController.php
cat app/Http/Controllers/API/ApiController.php

# Check for any OpenAPI server examples in the codebase
rg -B 2 -A 2 "@OA\\Server" 

Length of output: 3442

app/Http/Controllers/API/v1/TransactionController.php (1)

Line range hint 314-327: Add type filtering and improve error handling.

The method should maintain consistency with other endpoints by including type filtering.

Apply this diff to improve the implementation:

-    public function destroy($id): JsonResponse
+    public function destroy($id, Request $request): JsonResponse
     {
-        $transaction = Transaction::find($id);
+        $type = $request->query('type');
+        $query = Transaction::query();
+        if (!empty($type)) {
+            if (!in_array($type, ['income', 'expense'])) {
+                return $this->failure('Invalid transaction type', 400);
+            }
+            $query->where('type', $type);
+        }
+        $transaction = $query->find($id);
 
         if (! $transaction) {
-            return $this->failure('Transaction not found', 404);
+            return $this->failure('Transaction not found', 404, [
+                'id' => $id,
+                'type' => $type
+            ]);
         }
 
         $this->userCanAccessResource($transaction);
 
-        $transaction->delete();
+        try {
+            $transaction->delete();
+        } catch (\Exception $e) {
+            return $this->failure('Failed to delete transaction', 500, ['error' => $e->getMessage()]);
+        }
 
         return $this->success(['message' => 'Transaction deleted successfully']);
     }
public/docs/api.json (1)

Line range hint 1479-1482: Update required fields in Transaction schema.

The required fields list is outdated and includes the removed date field.

Apply this diff to fix the required fields:

                 "required": [
                     "id",
                     "type",
-                    "date"
+                    "amount",
+                    "datetime"
                 ],
♻️ Duplicate comments (2)
database/migrations/2024_07_20_125016_create_transactions_table.php (1)

35-35: ⚠️ Potential issue

Fix incorrect table name in down() method.

The down() method attempts to drop 'incomes' table instead of 'transactions'.

🧰 Tools
🪛 GitHub Actions: Checks

[error] 14: Failed to create foreign key constraint 'transactions_wallet_id_foreign'. The referenced table 'wallets' does not exist. Ensure the 'wallets' table migration is created and runs before the transactions table migration.

public/docs/api.json (1)

957-959: ⚠️ Potential issue

Fix datetime format in OpenAPI specification.

The datetime field uses "date" format, but since it represents both date and time, it should use "date-time" format to comply with RFC3339.

Also applies to: 1490-1493

🧹 Nitpick comments (5)
tests/Feature/TransactionsTest.php (1)

20-32: Consider linking wallet and party to the same user.
Currently, the wallet and party are created for different, random users rather than for $this->user. This could lead to inconsistencies if the tested logic relies on ownership or associations.

app/Models/Transaction.php (2)

48-51: Add return type hints to accessor methods.

For better type safety and IDE support, add return type hints to the accessor methods.

Apply this diff to add return type hints:

-    public function getCategoriesAttribute()
+    public function getCategoriesAttribute(): \Illuminate\Database\Eloquent\Collection
     {
         return $this->categories()->get();
     }

-    public function getWalletAttribute()
+    public function getWalletAttribute(): ?\App\Models\Wallet
     {
         return $this->wallet()->first();
     }

-    public function getPartyAttribute()
+    public function getPartyAttribute(): ?\App\Models\Party
     {
         return $this->party()->first();
     }

Also applies to: 53-56, 58-61


63-66: Add return type hints to relationship methods.

For better type safety and IDE support, add return type hints to the relationship methods.

Apply this diff to add return type hints:

-    public function wallet()
+    public function wallet(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
         return $this->belongsTo(Wallet::class);
     }

-    public function party()
+    public function party(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
         return $this->belongsTo(Party::class);
     }

-    public function user()
+    public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
         return $this->belongsTo(User::class);
     }

Also applies to: 68-71, 73-76

app/Http/Controllers/API/ApiController.php (2)

41-55: Enhance type safety and error messaging.

Add type hints and improve the error message for better debugging.

Apply this diff to enhance the method:

-    public function userCanAccessResource($resource, $ownerKey = 'user_id')
+    public function userCanAccessResource(mixed $resource, string $ownerKey = 'user_id'): void
     {
         if (auth()->id() !== $resource->{$ownerKey}) {
-            abort(Response::HTTP_FORBIDDEN, 'You are not authorized to access this resource.');
+            abort(Response::HTTP_FORBIDDEN, 
+                sprintf('Access denied. Resource %s#%s belongs to a different user.',
+                    get_class($resource),
+                    $resource->id ?? 'unknown'
+                )
+            );
         }
     }

57-85: Enhance type safety and error messaging in validation.

Add return type hint and improve error messages for better debugging.

Apply this diff to enhance the method:

-    protected function validateRequest(Request $request, array $rules): array
+    protected function validateRequest(Request $request, array $rules): array
     {
         $validator = Validator::make($request->all(), $rules);
 
         if ($validator->fails()) {
             $errors = $validator->errors();
 
             if ($errors->hasAny(array_keys($rules))) {
                 return [
                     'isValidated' => false,
-                    'message' => 'Server failed to validate request.',
+                    'message' => sprintf('Validation failed for fields: %s', implode(', ', array_keys($errors->messages()))),
                     'code' => 422,
                     'errors' => $errors->toArray(),
                 ];
             }
 
             return [
                 'isValidated' => false,
-                'message' => 'Server unable to process request.',
+                'message' => 'Request validation failed for unknown fields.',
                 'code' => 400,
                 'errors' => $errors->toArray(),
             ];
         }
 
         return ['isValidated' => true, 'data' => $validator->validated()];
     }
🛑 Comments failed to post (9)
tests/Feature/TransactionsTest.php (1)

189-189: 🛠️ Refactor suggestion

Avoid relying on a random user ID.
Using User::find(rand(1, 5)) could yield null or a user that does not exist, creating flakiness in tests. Replace with User::factory()->create() to ensure a reliable user object.

tests/TestCase.php (1)

11-20: 💡 Codebase verification

⚠️ Test isolation is compromised across all test files

The current implementation affects all test files as they extend TestCase, but no database transaction handling is implemented. This will cause:

  • Shared database state between tests
  • Potential false positives/negatives due to data pollution
  • Hard-to-debug test failures

Recommended fixes:

  • Use Laravel's RefreshDatabase trait in TestCase.php
  • Remove the static flag and migration optimization
  • Let each test start with a fresh database state
🔗 Analysis chain

Consider test isolation implications.

While running migrations once improves performance, it could lead to test interdependence and state pollution. Consider:

  1. Using database transactions for test isolation
  2. Implementing a test-specific cleanup mechanism
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if tests are using database transactions
rg -l "RefreshDatabase|DatabaseTransactions" "tests/"

Length of output: 194


Script:

#!/bin/bash
# Check for actual trait usage and TestCase extension
echo "=== Checking trait imports ==="
rg "use .+\\{.*(?:RefreshDatabase|DatabaseTransactions).*\\}" tests/

echo -e "\n=== Checking TestCase extension ==="
rg "class \w+ extends (?:TestCase|\\\Tests\\\TestCase)" tests/

echo -e "\n=== Finding all test files ==="
fd "Test\.php$" tests/

Length of output: 932

database/migrations/2024_07_20_125016_create_transactions_table.php (1)

14-27: 💡 Codebase verification

⚠️ Potential issue

Confirmed: Migration ordering needs to be fixed

The transactions table migration (125016) runs before both the parties (125104) and wallets (125116) tables are created, which will cause foreign key constraint failures. Rename the migrations to fix the ordering:

  1. Keep 2024_07_20_125104_create_parties_table.php as is
  2. Keep 2024_07_20_125116_create_wallets_table.php as is
  3. Rename transactions migration to 2024_07_20_125130_create_transactions_table.php
🔗 Analysis chain

Fix migration dependencies.

The pipeline failure indicates that the 'wallets' table doesn't exist when creating foreign key constraints. This migration should run after the wallets and parties tables are created.

Add a timestamp prefix to ensure correct ordering:

  1. First create wallets migration (e.g., 2024_07_20_125015_create_wallets_table.php)
  2. Then create parties migration
  3. Finally, this transactions migration
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check migration ordering
ls -la database/migrations/*_create_*_table.php

Length of output: 2039

🧰 Tools
🪛 GitHub Actions: Checks

[error] 14: Failed to create foreign key constraint 'transactions_wallet_id_foreign'. The referenced table 'wallets' does not exist. Ensure the 'wallets' table migration is created and runs before the transactions table migration.

app/Models/Transaction.php (2)

26-26: ⚠️ Potential issue

Add 'datetime' to required fields.

The datetime field should be required as it's essential for transaction records.

Apply this diff to update the required fields:

-    required: ['amount', 'type']
+    required: ['amount', 'type', 'datetime']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    required: ['amount', 'type', 'datetime']

18-18: ⚠️ Potential issue

Fix the datetime format in OpenAPI schema.

The datetime field uses 'date' format, but since it represents both date and time, it should use 'date-time' format to comply with RFC3339.

Apply this diff to fix the format:

-        new OA\Property(property: 'datetime', type: 'string', format: 'date', description: 'Date and time of the transaction'),
+        new OA\Property(property: 'datetime', type: 'string', format: 'date-time', description: 'Date and time of the transaction'),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        new OA\Property(property: 'datetime', type: 'string', format: 'date-time', description: 'Date and time of the transaction'),
app/Http/Controllers/API/v1/TransactionController.php (4)

173-188: 🛠️ Refactor suggestion

Refactor duplicate query logic and ensure consistent type filtering.

The method contains duplicate query logic and inconsistent type filtering.

Apply this diff to improve the implementation:

-        if (empty($type)) {
-            $transaction = Transaction::find($id);
-        } else {
-            $transaction = Transaction::where('type', $type)->find($id);
-        }
+        $query = Transaction::query();
+        if (!empty($type)) {
+            $query->where('type', $type);
+        }
+        $transaction = $query->find($id);
 
         if (! $transaction) {
             return $this->failure('Transaction not found', 404);
         }
 
         $this->userCanAccessResource($transaction);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        if (! empty($type) && ! in_array($type, ['income', 'expense'])) {
            return $this->failure('Invalid transaction type', 400);
        }

        $query = Transaction::query();
        if (!empty($type)) {
            $query->where('type', $type);
        }
        $transaction = $query->find($id);

        if (! $transaction) {
            return $this->failure('Transaction not found', 404);
        }

        $this->userCanAccessResource($transaction);

49-63: 🛠️ Refactor suggestion

Refactor duplicate query logic and add limit validation.

The method contains duplicate query logic and lacks validation for the limit parameter.

Apply this diff to improve the implementation:

     public function index(Request $request): JsonResponse
     {
         $type = $request->query('type');
-        $limit = $request->query('limit', 20);
+        $limit = min(max((int) $request->query('limit', 20), 1), 100);
 
         if (! empty($type) && ! in_array($type, ['income', 'expense'])) {
             return $this->failure('Invalid transaction type', 400);
         }
 
-        if (empty($type)) {
-            $transactions = auth()->user()->transactions()->paginate($limit);
+        $query = auth()->user()->transactions();
 
-            return $this->success($transactions);
-        } else {
-            $transactions = auth()->user()->transactions()->where('type', $type)->paginate($limit);
+        if (!empty($type)) {
+            $query->where('type', $type);
         }
 
+        $transactions = $query->paginate($limit);
         return $this->success($transactions);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        $type = $request->query('type');
        $limit = min(max((int) $request->query('limit', 20), 1), 100);

        if (! empty($type) && ! in_array($type, ['income', 'expense'])) {
            return $this->failure('Invalid transaction type', 400);
        }

        $query = auth()->user()->transactions();

        if (!empty($type)) {
            $query->where('type', $type);
        }

        $transactions = $query->paginate($limit);
        return $this->success($transactions);

100-130: 🛠️ Refactor suggestion

Add transaction to error response and handle race conditions.

The method should include transaction details in error responses and handle potential race conditions in category sync.

Apply this diff to improve the implementation:

     public function store(Request $request): JsonResponse
     {
+        \DB::beginTransaction();
+        try {
             $validationResult = $this->validateRequest($request, [
                 'amount' => 'required|numeric|min:0.01',
                 'type' => 'required|string|in:income,expense',
                 'description' => 'nullable|string',
                 'datetime' => 'nullable|date',
                 'group_id' => 'nullable|integer|exists:groups,id',
                 'party_id' => 'nullable|integer|exists:parties,id',
                 'wallet_id' => 'nullable|integer|exists:wallets,id',
                 'categories' => 'nullable|array',
                 'categories.*' => 'integer|exists:categories,id',
             ]);
 
             if (! $validationResult['isValidated']) {
-                return $this->failure($validationResult['message'], $validationResult['code'], $validationResult['errors']);
+                return $this->failure(
+                    $validationResult['message'],
+                    $validationResult['code'],
+                    array_merge($validationResult['errors'], ['request_data' => $request->all()])
+                );
             }
 
             $request = $validationResult['data'];
 
             $categories = [];
             if (isset($request['categories'])) {
                 $categories = $request['categories'];
                 unset($request['categories']);
             }
 
             $transaction = auth()->user()->transactions()->create($request);
 
             if (! empty($categories)) {
                 $transaction->categories()->sync($categories);
             }
 
+            \DB::commit();
             return $this->success($transaction, statusCode: 201);
+        } catch (\Exception $e) {
+            \DB::rollBack();
+            return $this->failure('Failed to create transaction', 500, ['error' => $e->getMessage()]);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        $validationResult = $this->validateRequest($request, [
            'amount' => 'required|numeric|min:0.01',
            'type' => 'required|string|in:income,expense',
            'description' => 'nullable|string',
            'datetime' => 'nullable|date',
            'group_id' => 'nullable|integer|exists:groups,id',
            'party_id' => 'nullable|integer|exists:parties,id',
            'wallet_id' => 'nullable|integer|exists:wallets,id',
            'categories' => 'nullable|array',
            'categories.*' => 'integer|exists:categories,id',
        ]);

        \DB::beginTransaction();
        try {
            if (! $validationResult['isValidated']) {
                return $this->failure(
                    $validationResult['message'],
                    $validationResult['code'],
                    array_merge($validationResult['errors'], ['request_data' => $request->all()])
                );
            }

            $request = $validationResult['data'];

            $categories = [];
            if (isset($request['categories'])) {
                $categories = $request['categories'];
                unset($request['categories']);
            }

            $transaction = auth()->user()->transactions()->create($request);

            if (! empty($categories)) {
                $transaction->categories()->sync($categories);
            }

            \DB::commit();
            return $this->success($transaction, statusCode: 201);
        } catch (\Exception $e) {
            \DB::rollBack();
            return $this->failure('Failed to create transaction', 500, ['error' => $e->getMessage()]);
        }

245-276: 🛠️ Refactor suggestion

Add transaction to error response, handle race conditions, and ensure consistent type filtering.

The method needs several improvements for better error handling and consistency.

Apply this diff to improve the implementation:

     public function update(Request $request, $id): JsonResponse
     {
+        \DB::beginTransaction();
+        try {
             $validationResult = $this->validateRequest($request, [
                 'amount' => 'nullable|numeric|min:0.01',
                 'type' => 'nullable|string|in:income,expense',
                 'datetime' => 'nullable|date',
                 'description' => 'nullable|string',
                 'party_id' => 'nullable|integer|exists:parties,id',
                 'wallet_id' => 'nullable|integer|exists:wallets,id',
                 'group_id' => 'nullable|integer|exists:groups,id',
                 'categories' => 'nullable|array',
                 'categories.*' => 'integer|exists:categories,id',
             ]);
 
             if (! $validationResult['isValidated']) {
-                return $this->failure($validationResult['message'], $validationResult['code'], $validationResult['errors']);
+                return $this->failure(
+                    $validationResult['message'],
+                    $validationResult['code'],
+                    array_merge($validationResult['errors'], ['request_data' => $request->all()])
+                );
             }
 
             $validatedData = $validationResult['data'];
-            $transaction = Transaction::find($id);
+            $type = $request->query('type');
+            $query = Transaction::query();
+            if (!empty($type)) {
+                $query->where('type', $type);
+            }
+            $transaction = $query->find($id);
 
             if (! $transaction) {
                 return $this->failure('Transaction not found', 404);
             }
 
             $this->userCanAccessResource($transaction);
 
             $transaction->update(array_filter($validatedData, fn ($value) => $value !== null));
 
             if (isset($validatedData['categories'])) {
                 $transaction->categories()->sync($validatedData['categories']);
             }
 
+            \DB::commit();
             return $this->success($transaction, 200);
+        } catch (\Exception $e) {
+            \DB::rollBack();
+            return $this->failure('Failed to update transaction', 500, ['error' => $e->getMessage()]);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        $validationResult = $this->validateRequest($request, [
            'amount' => 'nullable|numeric|min:0.01',
            'type' => 'nullable|string|in:income,expense',
            'datetime' => 'nullable|date',
            'description' => 'nullable|string',
            'party_id' => 'nullable|integer|exists:parties,id',
            'wallet_id' => 'nullable|integer|exists:wallets,id',
            'group_id' => 'nullable|integer|exists:groups,id',
            'categories' => 'nullable|array',
            'categories.*' => 'integer|exists:categories,id',
        ]);

        \DB::beginTransaction();
        try {
            if (! $validationResult['isValidated']) {
                return $this->failure(
                    $validationResult['message'],
                    $validationResult['code'],
                    array_merge($validationResult['errors'], ['request_data' => $request->all()])
                );
            }

            $validatedData = $validationResult['data'];
            $type = $request->query('type');
            $query = Transaction::query();
            if (!empty($type)) {
                $query->where('type', $type);
            }
            $transaction = $query->find($id);

            if (! $transaction) {
                return $this->failure('Transaction not found', 404);
            }

            $this->userCanAccessResource($transaction);

            $transaction->update(array_filter($validatedData, fn ($value) => $value !== null));

            if (isset($validatedData['categories'])) {
                $transaction->categories()->sync($validatedData['categories']);
            }

            \DB::commit();
            return $this->success($transaction, 200);
        } catch (\Exception $e) {
            \DB::rollBack();
            return $this->failure('Failed to update transaction', 500, ['error' => $e->getMessage()]);
        }

nfebe added 2 commits January 26, 2025 15:18
Signed-off-by: nfebe <fenn25.fn@gmail.com>
Signed-off-by: nfebe <fenn25.fn@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
database/migrations/2024_07_20_125117_create_transactions_table.php (1)

20-25: Add indexes for foreign keys and timestamps.

Missing indexes on foreign keys and timestamps could impact query performance, especially for large datasets.

             $table->foreign('wallet_id')->references('id')->on('wallets')->onDelete('cascade');
             $table->foreign('party_id')->references('id')->on('parties')->onDelete('cascade');
             $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+            $table->index(['wallet_id']);
+            $table->index(['party_id']);
+            $table->index(['user_id']);
             $table->timestamps();
+            $table->index(['created_at', 'updated_at']);
app/Http/Controllers/API/ApiController.php (1)

60-60: Add return type hint to validateRequest method.

The method should explicitly declare its return type for better type safety.

-    protected function validateRequest(Request $request, array $rules): array
+    protected function validateRequest(Request $request, array $rules): array{isValidated: bool, message?: string, code?: int, errors?: array<string, array<int, string>>, data?: array}
app/Http/Controllers/API/v1/TransactionController.php (1)

124-128: Optimize categories relationship loading.

Loading categories separately could lead to N+1 query issues. Consider eager loading the relationship.

-        $transaction = auth()->user()->transactions()->create($request);
+        $transaction = auth()->user()->transactions()->create($request);
+        $transaction->load('categories');

         if (! empty($categories)) {
             $transaction->categories()->sync($categories);
+            $transaction->load('categories');
         }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b106a8 and ed7e2aa.

📒 Files selected for processing (4)
  • app/Http/Controllers/API/ApiController.php (2 hunks)
  • app/Http/Controllers/API/v1/TransactionController.php (9 hunks)
  • app/Models/User.php (1 hunks)
  • database/migrations/2024_07_20_125117_create_transactions_table.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Models/User.php
🔇 Additional comments (2)
app/Http/Controllers/API/v1/TransactionController.php (2)

73-82: Sync OpenAPI required fields with validation rules.

The OpenAPI documentation shows different required fields compared to the actual validation rules in the store method.

-                required: ['amount', 'type'],
+                required: ['amount', 'type', 'datetime'],

261-262: Add type filter to update method.

The update method should filter by type like show/destroy methods for consistency.

-        $transaction = Transaction::find($id);
+        $transaction = Transaction::where('type', $request->query('type'))->find($id);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
app/Listeners/UserRegistered.php (1)

33-38: ⚠️ Potential issue

Optimize group creation and add error handling.

The current implementation has several issues:

  1. Multiple database calls in the loop
  2. Missing slug generation (required by the Group model's Sluggable trait)
  3. No error handling for database operations

Here's an improved implementation:

-        foreach ($defaultGroups as $group) {
-            $user->groups()->create([
-                'name' => $group,
-                'description' => "Default group for $group",
-            ]);
-        }
+        try {
+            $groups = collect($defaultGroups)->map(function ($group) {
+                return [
+                    'name' => $group,
+                    'slug' => Str::slug($group),
+                    'description' => "Default group for $group",
+                    'created_at' => now(),
+                    'updated_at' => now(),
+                ];
+            })->all();
+            
+            $user->groups()->createMany($groups);
+        } catch (\Exception $e) {
+            \Log::error('Failed to create default groups for user: ' . $user->id, [
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }

Don't forget to add these imports:

use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
🧹 Nitpick comments (7)
app/Listeners/UserRegistered.php (2)

24-31: Consider moving default groups to configuration.

The hardcoded group names should be moved to configuration to allow for easier customization and maintenance.

Create a new config file config/groups.php:

<?php

return [
    'defaults' => [
        'General',
        'Personal',
        'Family',
        'Friends',
        'Work',
    ]
];

Then update the code to use the config:

-        $defaultGroups = [
-            'General',
-            'Personal',
-            'Family',
-            'Friends',
-            'Work',
-        ];
+        $defaultGroups = config('groups.defaults');

Line range hint 1-40: Add type hints and return types.

The class is missing type hints and return types which help with code maintainability and catch potential errors early.

     /**
      * Handle the event.
      *
+     * @param UserRegisteredEvent $event
      * @return void
      */
-    public function handle(UserRegisteredEvent $event)
+    public function handle(UserRegisteredEvent $event): void
app/Http/Controllers/API/ApiController.php (1)

61-89: Enhance error messages for better clarity.

While the validation logic is solid, consider making the error messages more specific to help clients better understand the validation failures.

Apply this diff to improve the error messages:

                     'isValidated' => false,
-                    'message' => 'Server failed to validate request.',
+                    'message' => 'Validation failed. Please check the provided data.',
                     'code' => 422,
                     'errors' => $errors->toArray(),

                     'isValidated' => false,
-                    'message' => 'Server unable to process request.',
+                    'message' => 'Invalid request format. Please check the request structure.',
                     'code' => 400,
                     'errors' => $errors->toArray(),
database/seeders/UserSeeder.php (2)

5-9: Consider organizing imports alphabetically for better readability.

While not critical, organizing imports alphabetically is a common convention that improves code maintainability.

-use App\Models\Category;
-use App\Models\Group;
-use App\Models\Party;
-use App\Models\User;
-use App\Models\Wallet;
+use App\Models\Category;
+use App\Models\Group;
+use App\Models\Party;
+use App\Models\User;
+use App\Models\Wallet;

22-36: Consider refactoring repeated factory calls into a reusable method.

The current implementation has duplicate patterns for creating related entities. This could be simplified for better maintainability.

 class UserSeeder extends Seeder
 {
+    private function createRelatedEntities(User $user, string $model, int $count = 5): void
+    {
+        $model::factory($count)->create([
+            'user_id' => $user->id,
+        ]);
+    }
+
     public function run(): void
     {
         foreach (range(1, 10) as $index) {
             $user = User::factory()->create([
                 'email' => "user{$index}@trakli.io",
                 'username' => "user{$index}",
             ]);

-            Party::factory(5)->create([
-                'user_id' => $user->id,
-            ]);
-
-            Category::factory(5)->create([
-                'user_id' => $user->id,
-            ]);
-
-            Wallet::factory(5)->create([
-                'user_id' => $user->id,
-            ]);
-
-            Group::factory(5)->create([
-                'user_id' => $user->id,
-            ]);
+            $this->createRelatedEntities($user, Party::class);
+            $this->createRelatedEntities($user, Category::class);
+            $this->createRelatedEntities($user, Wallet::class);
+            $this->createRelatedEntities($user, Group::class);
         }
     }
 }
tests/Feature/TransactionsTest.php (2)

34-58: Enhance test coverage for transaction creation.

The test verifies the response structure but doesn't assert the actual values. Also, the test for the categories feature is missing.

Add these assertions to strengthen the test:

         $response->assertStatus(201)
             ->assertJsonStructure([
                 'success',
                 'data' => [
                     'amount',
                     'wallet_id',
                     'party_id',
                     'datetime',
                 ],
                 'message',
-            ]);
+            ])
+            ->assertJson([
+                'data' => [
+                    'amount' => 100,
+                    'type' => 'expense',
+                    'wallet_id' => $this->wallet->id,
+                    'party_id' => $this->party->id,
+                    'datetime' => '2025-01-01',
+                ]
+            ]);

         $this->assertDatabaseHas('transactions', ['id' => $response->json('data.id')]);
+        
+        // Test with categories
+        $categories = Category::factory()->count(2)->create(['user_id' => $this->user->id]);
+        $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
+            'type' => 'expense',
+            'amount' => 100,
+            'wallet_id' => $this->wallet->id,
+            'party_id' => $this->party->id,
+            'datetime' => '2025-01-01',
+            'categories' => $categories->pluck('id')->toArray(),
+        ]);
+        
+        $response->assertStatus(201);
+        $transaction = Transaction::find($response->json('data.id'));
+        $this->assertEquals($categories->pluck('id')->toArray(), $transaction->categories->pluck('id')->toArray());

91-118: Enhance transaction update test coverage.

The test should verify the actual updated values and test the categories update feature.

Apply this diff to improve the test:

     public function test_api_user_can_update_their_transactions()
     {
-        $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
-            'type' => 'expense',
-            'amount' => 100,
-            'wallet_id' => $this->wallet->id,
-            'party_id' => $this->party->id,
-            'datetime' => '2025-01-01',
-        ]);
-
-        $response->assertStatus(201);
+        $transaction = $this->createTransaction('expense');
+        $categories = Category::factory()->count(2)->create(['user_id' => $this->user->id]);
 
-        $response = $this->actingAs($this->user)->putJson('/api/v1/transactions/'.$response->json('data.id'), [
+        $response = $this->actingAs($this->user)->putJson('/api/v1/transactions/'.$transaction['id'], [
             'amount' => 200,
+            'categories' => $categories->pluck('id')->toArray(),
         ]);
 
         $response->assertStatus(200)
             ->assertJsonStructure([
                 'success',
                 'data' => [
                     'amount',
                     'wallet_id',
                     'party_id',
                     'datetime',
                 ],
                 'message',
-            ]);
+            ])
+            ->assertJson([
+                'data' => [
+                    'amount' => 200,
+                    'type' => 'expense',
+                    'wallet_id' => $this->wallet->id,
+                    'party_id' => $this->party->id,
+                ]
+            ]);
+            
+        $updatedTransaction = Transaction::find($transaction['id']);
+        $this->assertEquals(200, $updatedTransaction->amount);
+        $this->assertEquals($categories->pluck('id')->toArray(), $updatedTransaction->categories->pluck('id')->toArray());
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed7e2aa and da79e3f.

📒 Files selected for processing (28)
  • app/Http/Controllers/API/ApiController.php (2 hunks)
  • app/Http/Controllers/API/v1/TransactionController.php (9 hunks)
  • app/Listeners/UserRegistered.php (1 hunks)
  • app/Models/Category.php (1 hunks)
  • app/Models/Expense.php (0 hunks)
  • app/Models/Group.php (2 hunks)
  • app/Models/Income.php (0 hunks)
  • app/Models/Party.php (1 hunks)
  • app/Models/Transaction.php (2 hunks)
  • app/Models/User.php (1 hunks)
  • app/Models/Wallet.php (1 hunks)
  • database/factories/CategoryFactory.php (1 hunks)
  • database/factories/GroupFactory.php (1 hunks)
  • database/factories/PartyFactory.php (1 hunks)
  • database/factories/UserFactory.php (2 hunks)
  • database/factories/WalletFactory.php (1 hunks)
  • database/migrations/2024_07_20_125016_create_incomes_table.php (0 hunks)
  • database/migrations/2024_07_20_125030_create_groups_table.php (1 hunks)
  • database/migrations/2024_07_20_125104_create_parties_table.php (1 hunks)
  • database/migrations/2024_07_20_125116_create_wallets_table.php (1 hunks)
  • database/migrations/2024_07_20_125117_create_transactions_table.php (1 hunks)
  • database/migrations/2024_07_20_125455_create_categories_table.php (1 hunks)
  • database/migrations/2025_01_25_232422_create_categorizables.php (2 hunks)
  • database/seeders/UserSeeder.php (1 hunks)
  • public/docs/api.json (11 hunks)
  • tests/Feature/AuthControllerTest.php (2 hunks)
  • tests/Feature/TransactionsTest.php (1 hunks)
  • tests/TestCase.php (1 hunks)
💤 Files with no reviewable changes (3)
  • app/Models/Income.php
  • database/migrations/2024_07_20_125016_create_incomes_table.php
  • app/Models/Expense.php
🚧 Files skipped from review as they are similar to previous changes (18)
  • app/Models/Party.php
  • database/factories/GroupFactory.php
  • database/migrations/2024_07_20_125104_create_parties_table.php
  • database/migrations/2024_07_20_125455_create_categories_table.php
  • app/Models/Wallet.php
  • tests/TestCase.php
  • app/Models/Category.php
  • database/factories/PartyFactory.php
  • tests/Feature/AuthControllerTest.php
  • database/factories/CategoryFactory.php
  • database/factories/WalletFactory.php
  • database/factories/UserFactory.php
  • database/migrations/2024_07_20_125116_create_wallets_table.php
  • app/Models/Group.php
  • database/migrations/2024_07_20_125030_create_groups_table.php
  • database/migrations/2025_01_25_232422_create_categorizables.php
  • app/Models/User.php
  • database/migrations/2024_07_20_125117_create_transactions_table.php
🔇 Additional comments (14)
app/Models/Transaction.php (4)

19-24: LGTM! Well-structured categories property.

The categories property is correctly defined as an array of integers with clear description.


32-44: LGTM! Well-organized fillable attributes.

The fillable attributes are comprehensive and alphabetically ordered for better readability.


46-61: LGTM! Accessor methods are well-implemented.

The accessor methods correctly use:

  • first() for single relationships (wallet, party)
  • get() for collection relationships (categories)

63-81: LGTM! Well-structured relationships.

The relationships are correctly defined:

  • belongsTo for wallet, party, and user
  • morphToMany for categories, enabling flexible categorization across different models
app/Http/Controllers/API/ApiController.php (2)

Line range hint 12-38: LGTM! Well-defined security scheme.

The OpenAPI security attributes correctly define bearer token authentication with proper documentation.


41-59: LGTM! Robust resource access control.

The method provides comprehensive access control by:

  • Checking for null resources
  • Verifying resource ownership
tests/Feature/TransactionsTest.php (1)

120-154: Replace hardcoded IDs with factory-created entities.

Apply this diff to fix the hardcoded IDs:

     public function test_api_user_cannot_create_transaction_with_invalid_type()
     {
         $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
             'type' => 'invalid_type',
             'amount' => 100,
-            'wallet_id' => 1,
+            'wallet_id' => $this->wallet->id,
             'datetime' => '2025-01-01',
         ]);
 
         $response->assertStatus(422)
             ->assertJsonValidationErrors(['type']);
     }
 
     public function test_api_user_cannot_create_transaction_with_invalid_amount()
     {
         $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
             'type' => 'expense',
             'amount' => -100,
-            'wallet_id' => 1,
+            'wallet_id' => $this->wallet->id,
             'datetime' => '2025-01-01',
         ]);
app/Http/Controllers/API/v1/TransactionController.php (5)

50-60: Include pagination metadata in response.

Apply this diff to include pagination metadata:

         $query = auth()->user()->transactions();
         if (! empty($type)) {
             $query->where('type', $type);
         }
         $transactions = $query->paginate($limit);
 
-        return $this->success($transactions);
+        return $this->success([
+            'data' => $transactions->items(),
+            'meta' => [
+                'current_page' => $transactions->currentPage(),
+                'total' => $transactions->total(),
+                'per_page' => $transactions->perPage(),
+                'last_page' => $transactions->lastPage(),
+            ]
+        ]);

278-278: Watch out for zero values with array_filter.

Apply this diff to preserve zero values:

-        $transaction->update(array_filter($validatedData));
+        $transaction->update(array_filter($validatedData, fn($value) => $value !== null));

176-181: Inconsistent type filter for show method.

Apply this diff to consistently apply the type filter:

         $query = Transaction::query();
         if (! empty($type)) {
             $query->where('type', $type);
         }
 
-        $transaction = $query->find($id);
+        $transaction = $query->where('id', $id)->first();

221-227: Mismatch between documentation and code.

The OpenAPI documentation shows these fields as required, but the validation makes them optional. Update the documentation to match the code:

                 required: [
-                    'date',
-                    'party_id',
-                    'wallet_id',
-                    'amount',
-                    'group_id'
                 ],

315-327: LGTM! Good error handling implementation.

The error handling using HttpException and proper status codes is well implemented.

public/docs/api.json (2)

957-959: Fix datetime format in OpenAPI specification.

The datetime field uses "date" format, but since it represents both date and time, it should use "date-time" format to comply with RFC3339.

Apply this diff to fix the format:

                     "datetime": {
                         "type": "string",
-                        "format": "date"
+                        "format": "date-time"
                     },

Also applies to: 1482-1485


1516-1521: LGTM! Well-structured security implementation.

The security scheme is properly defined with bearer token authentication and correctly applied globally with exceptions for authentication endpoints.

Also applies to: 1524-1528

Signed-off-by: nfebe <fenn25.fn@gmail.com>
nfebe added 2 commits January 26, 2025 17:30
Signed-off-by: nfebe <fenn25.fn@gmail.com>
Signed-off-by: nfebe <fenn25.fn@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
tests/Feature/TransactionsTest.php (2)

19-32: ⚠️ Potential issue

Fix test data ownership in setUp method.

The wallet and party are created for different users than the test user, which could lead to authorization issues.

     protected function setUp(): void
     {
         parent::setUp();
-        $this->wallet = User::factory()->create()->wallets()->create([
+        $this->user = User::factory()->create();
+        $this->wallet = $this->user->wallets()->create([
             'name' => 'Wallet',
             'balance' => 1000,
         ]);

-        $this->party = User::factory()->create()->parties()->create([
+        $this->party = $this->user->parties()->create([
             'name' => 'Party',
             'type' => 'personal',
         ]);
-        $this->user = User::factory()->create();
     }

98-132: 🛠️ Refactor suggestion

Replace hardcoded IDs in validation tests.

Using hardcoded IDs may cause test failures if these records don't exist.

     public function test_api_user_cannot_create_transaction_with_invalid_type()
     {
         $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
             'type' => 'invalid_type',
             'amount' => 100,
-            'wallet_id' => 1,
+            'wallet_id' => $this->wallet->id,
             'datetime' => '2025-01-01',
         ]);

     public function test_api_user_cannot_create_transaction_with_invalid_amount()
     {
         $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
             'type' => 'expense',
             'amount' => -100,
-            'wallet_id' => 1,
+            'wallet_id' => $this->wallet->id,
             'datetime' => '2025-01-01',
         ]);
app/Http/Controllers/API/v1/TransactionController.php (2)

50-62: 🛠️ Refactor suggestion

Include pagination metadata in response.

The paginated response should include metadata for better client-side handling.

         $query = auth()->user()->transactions();
         if (! empty($type)) {
             $query->where('type', $type);
         }
         $transactions = $query->paginate($limit);
-        return $this->success($transactions);
+        return $this->success([
+            'data' => $transactions->items(),
+            'meta' => [
+                'current_page' => $transactions->currentPage(),
+                'total' => $transactions->total(),
+                'per_page' => $transactions->perPage(),
+                'last_page' => $transactions->lastPage(),
+            ]
+        ]);

278-278: ⚠️ Potential issue

Watch out for zero values with array_filter.

array_filter($validatedData) will drop non-empty but falsy values like "0.0" or 0.

-        $transaction->update(array_filter($validatedData));
+        $transaction->update(array_filter($validatedData, fn($value) => $value !== null));
🧹 Nitpick comments (2)
app/Http/Controllers/API/ApiController.php (1)

61-89: Consider enhancing error message clarity.

The error messages could be more specific to help API consumers better understand the validation failures.

                     'isValidated' => false,
-                    'message' => 'Server failed to validate request.',
+                    'message' => 'Validation failed: One or more required fields are invalid.',
                     'code' => 422,
                     'errors' => $errors->toArray(),

                     'isValidated' => false,
-                    'message' => 'Server unable to process request.',
+                    'message' => 'Bad request: The provided data is invalid.',
                     'code' => 400,
public/docs/api.json (1)

1530-1533: Enhance tag descriptions for better API documentation.

The Authentication tag's description only mentions password reset endpoints, but it also includes registration and login endpoints.

Apply this diff to improve the description:

        {
            "name": "Authentication",
-            "description": "Endpoints for password reset"
+            "description": "Endpoints for user registration, login, and password reset"
        },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e03efe1 and 1701c65.

📒 Files selected for processing (10)
  • app/Http/Controllers/API/ApiController.php (2 hunks)
  • app/Http/Controllers/API/v1/TransactionController.php (9 hunks)
  • app/Models/Party.php (1 hunks)
  • app/Models/User.php (1 hunks)
  • app/Models/Wallet.php (1 hunks)
  • database/migrations/2024_07_20_125117_create_transactions_table.php (1 hunks)
  • public/docs/api.json (11 hunks)
  • tests/Feature/AuthControllerTest.php (2 hunks)
  • tests/Feature/TransactionsTest.php (1 hunks)
  • tests/TestCase.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/Models/Party.php
  • tests/Feature/AuthControllerTest.php
  • app/Models/Wallet.php
  • tests/TestCase.php
  • database/migrations/2024_07_20_125117_create_transactions_table.php
  • app/Models/User.php
🔇 Additional comments (7)
app/Http/Controllers/API/ApiController.php (2)

Line range hint 12-38: LGTM! Well-structured OpenAPI security configuration.

The OpenAPI attributes are well-organized, properly defining bearer token authentication as the security scheme.


41-59: LGTM! Robust resource access control implementation.

The userCanAccessResource method includes proper null checks and clear error messages. This matches the previously suggested implementation.

tests/Feature/TransactionsTest.php (2)

34-46: Replace hardcoded values in createTransaction helper.

The helper method uses hardcoded values which could make tests brittle.

-    private function createTransaction(string $type): array
+    private function createTransaction(string $type, float $amount = 100.00): array
     {
         $response = $this->actingAs($this->user)->postJson('/api/v1/transactions', [
             'type' => $type,
-            'amount' => 100,
+            'amount' => $amount,
             'wallet_id' => $this->wallet->id,
             'party_id' => $this->party->id,
             'datetime' => '2025-01-01',
         ]);

57-75: Improve transaction retrieval test coverage.

The test has several areas for improvement:

  1. Missing assertions for the response data
  2. Not testing the limit parameter thoroughly
     public function test_api_user_can_get_their_transactions()
     {
-        $this->createTransaction('expense');
-        $this->createTransaction('income');
+        $expense = $this->createTransaction('expense');
+        $income = $this->createTransaction('income');
 
         $response = $this->actingAs($this->user)->getJson('/api/v1/transactions?type=expense');
-        $response->assertStatus(200);
+        $response->assertStatus(200)
+            ->assertJsonCount(1, 'data')
+            ->assertJsonPath('data.0.id', $expense['id']);
 
         $response = $this->actingAs($this->user)->getJson('/api/v1/transactions?type=income');
-        $response->assertStatus(200);
+        $response->assertStatus(200)
+            ->assertJsonCount(1, 'data')
+            ->assertJsonPath('data.0.id', $income['id']);
 
         // Test limit parameter
-        $this->createTransaction('expense');
-        $this->createTransaction('expense');
+        $expense2 = $this->createTransaction('expense');
+        $expense3 = $this->createTransaction('expense');
 
         $response = $this->actingAs($this->user)->getJson('/api/v1/transactions?type=expense&limit=2');
         $response->assertStatus(200)
-            ->assertJsonCount(2, 'data.data');
+            ->assertJsonCount(2, 'data')
+            ->assertJsonPath('data.0.id', $expense3['id'])
+            ->assertJsonPath('data.1.id', $expense2['id']);
+
+        // Test default limit
+        $response = $this->actingAs($this->user)->getJson('/api/v1/transactions?type=expense');
+        $response->assertStatus(200)
+            ->assertJsonCount(3, 'data');
app/Http/Controllers/API/v1/TransactionController.php (1)

Line range hint 957-959: Fix datetime format in OpenAPI specification.

The datetime field uses "date" format, but since it represents both date and time, it should use "date-time" format to comply with RFC3339.

                                    "datetime": {
                                        "type": "string",
-                                        "format": "date"
+                                        "format": "date-time"
                                    },

Also applies to: 1482-1486

public/docs/api.json (2)

73-74: LGTM! Security implementation follows best practices.

The security implementation correctly:

  • Excludes authentication endpoints from bearer token requirement
  • Implements global bearer token authentication for protected endpoints
  • Uses standard HTTP Bearer scheme

Also applies to: 123-124, 174-175, 238-239, 1515-1528


957-959: Fix datetime format in OpenAPI specification.

The datetime field uses "date" format, but since it represents both date and time, it should use "date-time" format to comply with RFC3339.

Apply this diff to fix the format:

                                    "datetime": {
                                        "type": "string",
-                                        "format": "date"
+                                        "format": "date-time"
                                    },

Also applies to: 1482-1485

Comment on lines +940 to +975
"type"
],
"properties": {
"amount": {
"type": "number",
"format": "float"
},
"type": {
"type": "string",
"enum": [
"income",
"expense"
]
},
"date": {
"description": {
"type": "string"
},
"datetime": {
"type": "string",
"format": "date"
},
"party_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"wallet_id": {
"type": "integer"
},
"amount": {
"type": "number",
"format": "float"
},
"group_id": {
"type": "integer"
},
"categories": {
"type": "array",
"items": {
"description": "Category ID array",
"type": "integer"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inconsistency in required fields between POST and PUT endpoints.

The POST endpoint requires only amount and type, while the PUT endpoint requires additional fields (date, party_id, wallet_id, group_id). This inconsistency could lead to confusion.

Apply this diff to align the required fields:

                                "required": [
                                    "date",
                                    "party_id",
                                    "wallet_id",
                                    "amount",
-                                    "group_id"
+                                    "type"
                                ],

Also, ensure that the Transaction schema's required fields match the endpoint requirements.

Also applies to: 1093-1103, 1458-1458, 1487-1493

Comment on lines +896 to +905
},
{
"name": "limit",
"in": "query",
"description": "Number of transactions to fetch",
"required": true,
"schema": {
"type": "integer",
"default": 20
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider making the limit parameter optional.

The limit parameter is marked as required but has a default value. This is contradictory as required parameters should not have default values.

Apply this diff to fix the parameter definition:

                    {
                        "name": "limit",
                        "in": "query",
                        "description": "Number of transactions to fetch",
-                        "required": true,
+                        "required": false,
                        "schema": {
                            "type": "integer",
                            "default": 20
                        }
                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
},
{
"name": "limit",
"in": "query",
"description": "Number of transactions to fetch",
"required": true,
"schema": {
"type": "integer",
"default": 20
}
},
{
"name": "limit",
"in": "query",
"description": "Number of transactions to fetch",
"required": false,
"schema": {
"type": "integer",
"default": 20
}

Comment on lines +64 to +90
protected function validateRequest(Request $request, array $rules): array
{
$validator = Validator::make($request->all(), $rules);

if ($validator->fails()) {
$errors = $validator->errors();

if ($errors->hasAny(array_keys($rules))) {
return [
'isValidated' => false,
'message' => 'Server failed to validate request.',
'code' => 422,
'errors' => $errors->toArray(),
];
}

return [
'isValidated' => false,
'message' => 'Server unable to process request.',
'code' => 400,
'errors' => $errors->toArray(),
];
}

return ['isValidated' => true, 'data' => $validator->validated()];
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use Form Requests for this

Copy link
Contributor Author

@nfebe nfebe Jan 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we would have to write FormRequests for each endpoint! I did not want to do that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. It makes the code cleaner in my own opinion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. It usually does make things cleaner when you have large and complex requests. There is no need to have 4 different FormRequests (4 + files) for the CRUD operations in this file and then we would have to repeat this across all the models, in reality only a few of the requests (e.g store) are big a enough to need the FormRequests which I do use in other code bases but in this case I think this small function takes the bull by the horn we can use to validate all the simple requests based on the rules passed without adding any new files to the project.

@nfebe nfebe merged commit 30e9a26 into main Jan 26, 2025
2 checks passed
@nfebe nfebe deleted the fix/transactions branch January 26, 2025 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants