From 635cde021b59c9078e72882327c17d500503d22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Fri, 7 Nov 2025 15:45:28 +0000 Subject: [PATCH] feat: Add financial models and seeders --- app/AccountType.php | 12 +++++ app/Enums/AccountType.php | 12 +++++ app/Models/Account.php | 47 ++++++++++++++++++ app/Models/Bank.php | 25 ++++++++++ app/Models/Category.php | 32 ++++++++++++ app/Models/Transaction.php | 49 +++++++++++++++++++ app/Models/User.php | 16 ++++++ database/factories/AccountFactory.php | 31 ++++++++++++ database/factories/BankFactory.php | 25 ++++++++++ database/factories/CategoryFactory.php | 27 ++++++++++ database/factories/TransactionFactory.php | 35 +++++++++++++ .../2025_11_07_150038_create_banks_table.php | 31 ++++++++++++ ...025_11_07_150122_create_accounts_table.php | 34 +++++++++++++ ...5_11_07_150613_create_categories_table.php | 32 ++++++++++++ ...11_07_150659_create_transactions_table.php | 37 ++++++++++++++ database/seeders/AccountSeeder.php | 45 +++++++++++++++++ database/seeders/BankSeeder.php | 31 ++++++++++++ database/seeders/CategorySeeder.php | 44 +++++++++++++++++ database/seeders/TransactionSeeder.php | 43 ++++++++++++++++ 19 files changed, 608 insertions(+) create mode 100644 app/AccountType.php create mode 100644 app/Enums/AccountType.php create mode 100644 app/Models/Account.php create mode 100644 app/Models/Bank.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Transaction.php create mode 100644 database/factories/AccountFactory.php create mode 100644 database/factories/BankFactory.php create mode 100644 database/factories/CategoryFactory.php create mode 100644 database/factories/TransactionFactory.php create mode 100644 database/migrations/2025_11_07_150038_create_banks_table.php create mode 100644 database/migrations/2025_11_07_150122_create_accounts_table.php create mode 100644 database/migrations/2025_11_07_150613_create_categories_table.php create mode 100644 database/migrations/2025_11_07_150659_create_transactions_table.php create mode 100644 database/seeders/AccountSeeder.php create mode 100644 database/seeders/BankSeeder.php create mode 100644 database/seeders/CategorySeeder.php create mode 100644 database/seeders/TransactionSeeder.php diff --git a/app/AccountType.php b/app/AccountType.php new file mode 100644 index 00000000..07d591d4 --- /dev/null +++ b/app/AccountType.php @@ -0,0 +1,12 @@ + */ + use HasFactory, SoftDeletes; + + protected $fillable = [ + 'user_id', + 'name', + 'name_iv', + 'bank_id', + 'currency_code', + 'type', + ]; + + protected function casts(): array + { + return [ + 'type' => AccountType::class, + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function bank(): BelongsTo + { + return $this->belongsTo(Bank::class); + } + + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class); + } +} diff --git a/app/Models/Bank.php b/app/Models/Bank.php new file mode 100644 index 00000000..578c8a48 --- /dev/null +++ b/app/Models/Bank.php @@ -0,0 +1,25 @@ + */ + use HasFactory, SoftDeletes; + + protected $fillable = [ + 'name', + 'name_iv', + 'logo', + ]; + + public function accounts(): HasMany + { + return $this->hasMany(Account::class); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 00000000..c8149b5b --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,32 @@ + */ + use HasFactory, SoftDeletes; + + protected $fillable = [ + 'name', + 'icon', + 'color', + 'user_id', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class); + } +} diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php new file mode 100644 index 00000000..65199b90 --- /dev/null +++ b/app/Models/Transaction.php @@ -0,0 +1,49 @@ + */ + use HasFactory; + + protected $fillable = [ + 'user_id', + 'account_id', + 'category_id', + 'description', + 'description_iv', + 'transaction_date', + 'amount', + 'currency_code', + 'notes', + 'notes_iv', + ]; + + protected function casts(): array + { + return [ + 'transaction_date' => 'date', + 'amount' => 'decimal:2', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function account(): BelongsTo + { + return $this->belongsTo(Account::class); + } + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f58a4f3c..3de6f14e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -56,4 +57,19 @@ class User extends Authenticatable { return $this->hasOne(EncryptedMessage::class); } + + public function transactions(): HasMany + { + return $this->hasMany(Transaction::class); + } + + public function accounts(): HasMany + { + return $this->hasMany(Account::class); + } + + public function categories(): HasMany + { + return $this->hasMany(Category::class); + } } diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php new file mode 100644 index 00000000..5a50a7c3 --- /dev/null +++ b/database/factories/AccountFactory.php @@ -0,0 +1,31 @@ + + */ +class AccountFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'name' => fake()->words(2, true).' Account', + 'name_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'bank_id' => Bank::factory(), + 'currency_code' => fake()->randomElement(['USD', 'EUR', 'GBP', 'JPY']), + 'type' => fake()->randomElement(AccountType::cases()), + ]; + } +} diff --git a/database/factories/BankFactory.php b/database/factories/BankFactory.php new file mode 100644 index 00000000..14761298 --- /dev/null +++ b/database/factories/BankFactory.php @@ -0,0 +1,25 @@ + + */ +class BankFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->company(), + 'name_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'logo' => fake()->imageUrl(200, 200, 'business'), + ]; + } +} diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 00000000..288adff9 --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,27 @@ + + */ +class CategoryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->randomElement(['Food', 'Transport', 'Entertainment', 'Shopping', 'Healthcare', 'Utilities', 'Travel', 'Education']), + 'icon' => fake()->randomElement(['🍔', '🚗', '🎮', '🛒', '💊', '💡', '✈️', '📚']), + 'color' => fake()->hexColor(), + 'user_id' => User::factory(), + ]; + } +} diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php new file mode 100644 index 00000000..935d20c8 --- /dev/null +++ b/database/factories/TransactionFactory.php @@ -0,0 +1,35 @@ + + */ +class TransactionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'account_id' => Account::factory(), + 'category_id' => Category::factory(), + 'description' => fake()->sentence(), + 'description_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'transaction_date' => fake()->dateTimeBetween('-1 year', 'now'), + 'amount' => fake()->randomFloat(2, -1000, 1000), + 'currency_code' => fake()->randomElement(['USD', 'EUR', 'GBP', 'JPY']), + 'notes' => fake()->optional()->paragraph(), + 'notes_iv' => fake()->optional()->regexify('[A-Za-z0-9]{16}'), + ]; + } +} diff --git a/database/migrations/2025_11_07_150038_create_banks_table.php b/database/migrations/2025_11_07_150038_create_banks_table.php new file mode 100644 index 00000000..56b287df --- /dev/null +++ b/database/migrations/2025_11_07_150038_create_banks_table.php @@ -0,0 +1,31 @@ +id(); + $table->text('name'); + $table->string('name_iv', 16); + $table->string('logo')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('banks'); + } +}; diff --git a/database/migrations/2025_11_07_150122_create_accounts_table.php b/database/migrations/2025_11_07_150122_create_accounts_table.php new file mode 100644 index 00000000..e3cd8ebf --- /dev/null +++ b/database/migrations/2025_11_07_150122_create_accounts_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->text('name'); + $table->string('name_iv', 16); + $table->foreignId('bank_id')->constrained()->onDelete('cascade'); + $table->string('currency_code', 3); + $table->string('type'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('accounts'); + } +}; diff --git a/database/migrations/2025_11_07_150613_create_categories_table.php b/database/migrations/2025_11_07_150613_create_categories_table.php new file mode 100644 index 00000000..7790a73e --- /dev/null +++ b/database/migrations/2025_11_07_150613_create_categories_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('icon')->nullable(); + $table->string('color')->nullable(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2025_11_07_150659_create_transactions_table.php b/database/migrations/2025_11_07_150659_create_transactions_table.php new file mode 100644 index 00000000..474ed961 --- /dev/null +++ b/database/migrations/2025_11_07_150659_create_transactions_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('account_id')->constrained()->onDelete('cascade'); + $table->foreignId('category_id')->constrained()->onDelete('cascade'); + $table->text('description'); + $table->string('description_iv', 16); + $table->date('transaction_date'); + $table->decimal('amount', 15, 2); + $table->string('currency_code', 3); + $table->text('notes')->nullable(); + $table->string('notes_iv', 16)->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('transactions'); + } +}; diff --git a/database/seeders/AccountSeeder.php b/database/seeders/AccountSeeder.php new file mode 100644 index 00000000..685d1b35 --- /dev/null +++ b/database/seeders/AccountSeeder.php @@ -0,0 +1,45 @@ +isEmpty() || $banks->isEmpty()) { + return; + } + + foreach ($users as $user) { + Account::create([ + 'user_id' => $user->id, + 'name' => 'Main Checking', + 'name_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'bank_id' => $banks->random()->id, + 'currency_code' => 'USD', + 'type' => AccountType::Checking, + ]); + + Account::create([ + 'user_id' => $user->id, + 'name' => 'Savings Account', + 'name_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'bank_id' => $banks->random()->id, + 'currency_code' => 'USD', + 'type' => AccountType::Savings, + ]); + } + } +} diff --git a/database/seeders/BankSeeder.php b/database/seeders/BankSeeder.php new file mode 100644 index 00000000..c6437025 --- /dev/null +++ b/database/seeders/BankSeeder.php @@ -0,0 +1,31 @@ + 'Chase', 'logo' => null], + ['name' => 'Bank of America', 'logo' => null], + ['name' => 'Wells Fargo', 'logo' => null], + ['name' => 'Citibank', 'logo' => null], + ['name' => 'US Bank', 'logo' => null], + ]; + + foreach ($banks as $bank) { + Bank::create([ + 'name' => $bank['name'], + 'name_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'logo' => $bank['logo'], + ]); + } + } +} diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php new file mode 100644 index 00000000..78342beb --- /dev/null +++ b/database/seeders/CategorySeeder.php @@ -0,0 +1,44 @@ +isEmpty()) { + return; + } + + $categories = [ + ['name' => 'Food & Dining', 'icon' => '🍔', 'color' => '#FF6B6B'], + ['name' => 'Transportation', 'icon' => '🚗', 'color' => '#4ECDC4'], + ['name' => 'Entertainment', 'icon' => '🎮', 'color' => '#95E1D3'], + ['name' => 'Shopping', 'icon' => '🛒', 'color' => '#F38181'], + ['name' => 'Healthcare', 'icon' => '💊', 'color' => '#AA96DA'], + ['name' => 'Utilities', 'icon' => '💡', 'color' => '#FCBAD3'], + ['name' => 'Travel', 'icon' => '✈️', 'color' => '#FFFFD2'], + ['name' => 'Education', 'icon' => '📚', 'color' => '#A8D8EA'], + ]; + + foreach ($users as $user) { + foreach ($categories as $category) { + Category::create([ + 'name' => $category['name'], + 'icon' => $category['icon'], + 'color' => $category['color'], + 'user_id' => $user->id, + ]); + } + } + } +} diff --git a/database/seeders/TransactionSeeder.php b/database/seeders/TransactionSeeder.php new file mode 100644 index 00000000..6e508a45 --- /dev/null +++ b/database/seeders/TransactionSeeder.php @@ -0,0 +1,43 @@ +isEmpty() || $accounts->isEmpty() || $categories->isEmpty()) { + return; + } + + foreach ($accounts->take(5) as $account) { + for ($i = 0; $i < 10; $i++) { + Transaction::create([ + 'user_id' => $users->random()->id, + 'account_id' => $account->id, + 'category_id' => $categories->random()->id, + 'description' => fake()->sentence(), + 'description_iv' => fake()->regexify('[A-Za-z0-9]{16}'), + 'transaction_date' => fake()->dateTimeBetween('-3 months', 'now'), + 'amount' => fake()->randomFloat(2, -500, 500), + 'currency_code' => 'USD', + 'notes' => fake()->optional(0.3)->paragraph(), + 'notes_iv' => fake()->optional(0.3)->regexify('[A-Za-z0-9]{16}'), + ]); + } + } + } +}