Feat: Adds multi currency support (#88)
This commit is contained in:
@@ -41,28 +41,35 @@ class TransactionFactory extends Factory
|
||||
public function yearsAgo(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'date' => $this->faker->dateTimeBetween('-5 years', '-3 years')->format('Y-m-d'),
|
||||
'date' => now()->subYears($this->faker->numberBetween(3, 5))->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function lastYear(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'date' => now()->subYear()->format('Y-m-d'),
|
||||
'date' => now()->subYear()->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function lastMonth(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'date' => now()->subMonth()->format('Y-m-d'),
|
||||
'date' => now()->subMonth()->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function recent(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'date' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d'),
|
||||
'date' => now()->subDays($this->faker->numberBetween(3, 14))->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function date($date): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'date' => $date,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -80,6 +87,27 @@ class TransactionFactory extends Factory
|
||||
]);
|
||||
}
|
||||
|
||||
public function currency($currency): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'currency' => $currency,
|
||||
]);
|
||||
}
|
||||
|
||||
public function costBasis($cost_basis): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'cost_basis' => $cost_basis,
|
||||
]);
|
||||
}
|
||||
|
||||
public function salePrice($sale_price): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'sale_price' => $sale_price,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buy(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
|
||||
@@ -34,6 +34,10 @@ class UserFactory extends Factory
|
||||
'two_factor_recovery_codes' => null,
|
||||
'remember_token' => Str::random(10),
|
||||
'profile_photo_path' => null,
|
||||
'options' => [
|
||||
'display_currency' => 'USD',
|
||||
'locale' => 'en',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -46,4 +50,14 @@ class UserFactory extends Factory
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's currency.
|
||||
*/
|
||||
public function currency($currency): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => array_merge($attributes['options'], [
|
||||
'currency' => $currency,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ return new class extends Migration
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->string('profile_photo_path', 2048)->nullable();
|
||||
$table->boolean('admin')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
@@ -38,6 +39,5 @@ return new class extends Migration
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Database\Seeders\MarketDataSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMarketDataTable extends Migration
|
||||
@@ -34,10 +32,6 @@ class CreateMarketDataTable extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => MarketDataSeeder::class,
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,10 +20,6 @@ class CreateDailyChangeTable extends Migration
|
||||
$table->date('date');
|
||||
$table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade');
|
||||
$table->float('total_market_value', 12, 4)->nullable();
|
||||
$table->float('total_cost_basis', 12, 4)->nullable();
|
||||
$table->float('total_gain', 12, 4)->nullable();
|
||||
$table->float('total_dividends_earned', 12, 4)->nullable();
|
||||
$table->float('realized_gains', 12, 4)->nullable();
|
||||
$table->text('annotation')->nullable();
|
||||
|
||||
$table->primary(['date', 'portfolio_id']);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('admin')->nullable()->after('profile_photo_path');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('admin');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\CurrencyRate;
|
||||
use App\Models\Transaction;
|
||||
use Database\Seeders\CurrencySeeder;
|
||||
use Database\Seeders\MarketDataSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
/**
|
||||
* Add options column to users table
|
||||
*/
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->json('options')->default(json_encode([
|
||||
'locale' => config('app.locale', 'en'),
|
||||
'display_currency' => config('investbrain.base_currency', 'USD'),
|
||||
]))->after('profile_photo_path');
|
||||
});
|
||||
|
||||
/**
|
||||
* Add _base and currency column to market_data table
|
||||
*/
|
||||
Schema::table('market_data', function (Blueprint $table) {
|
||||
$table->float('market_value_base', 12, 4)->nullable()->after('market_value');
|
||||
$table->string('currency', 3)->default(config('investbrain.base_currency'))->after('market_value');
|
||||
});
|
||||
DB::table('market_data')->update([
|
||||
'market_value_base' => DB::raw('market_value'),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Add _base columns to transactions table
|
||||
*/
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->float('cost_basis_base', 12, 4)->nullable()->after('sale_price');
|
||||
$table->float('sale_price_base', 12, 4)->nullable()->after('cost_basis_base');
|
||||
});
|
||||
DB::table('transactions')->update([
|
||||
'cost_basis_base' => DB::raw('cost_basis'),
|
||||
'sale_price_base' => DB::raw('sale_price'),
|
||||
]);
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->float('cost_basis_base', 12, 4)->nullable(false)->change();
|
||||
});
|
||||
|
||||
/**
|
||||
* Add _base columns to dividends table
|
||||
*/
|
||||
Schema::table('dividends', function (Blueprint $table) {
|
||||
$table->float('dividend_amount_base', 12, 4)->nullable()->after('dividend_amount');
|
||||
});
|
||||
DB::table('dividends')->update([
|
||||
'dividend_amount_base' => DB::raw('dividend_amount'),
|
||||
]);
|
||||
Schema::table('dividends', function (Blueprint $table) {
|
||||
$table->float('dividend_amount_base', 12, 4)->nullable(false)->change();
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates currencies table
|
||||
*/
|
||||
Schema::create('currencies', function (Blueprint $table) {
|
||||
$table->string('currency', 3)->primary(); // ISO 4217
|
||||
$table->string('label');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates currency rates table
|
||||
*/
|
||||
Schema::create('currency_rates', function (Blueprint $table) {
|
||||
$table->date('date');
|
||||
$table->string('currency', 3);
|
||||
$table->float('rate', 12, 4);
|
||||
$table->timestamps();
|
||||
|
||||
$table->primary(['date', 'currency']);
|
||||
});
|
||||
|
||||
if (config('app.env') != 'testing') {
|
||||
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => CurrencySeeder::class,
|
||||
'--force' => true,
|
||||
]);
|
||||
|
||||
CurrencyRate::timeSeriesRates(
|
||||
'', // use fake currency to force
|
||||
Transaction::min('date')
|
||||
);
|
||||
|
||||
CurrencyRate::refreshCurrencyData();
|
||||
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => MarketDataSeeder::class,
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup daily change table
|
||||
*/
|
||||
if (Schema::hasColumn('daily_change', 'total_cost_basis')) {
|
||||
Schema::table('daily_change', function (Blueprint $table) {
|
||||
$table->dropColumn('total_cost_basis');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('daily_change', 'total_gain')) {
|
||||
Schema::table('daily_change', function (Blueprint $table) {
|
||||
$table->dropColumn('total_gain');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('daily_change', 'total_dividends_earned')) {
|
||||
Schema::table('daily_change', function (Blueprint $table) {
|
||||
$table->dropColumn('total_dividends_earned');
|
||||
});
|
||||
}
|
||||
if (Schema::hasColumn('daily_change', 'realized_gains')) {
|
||||
Schema::table('daily_change', function (Blueprint $table) {
|
||||
$table->dropColumn('realized_gains');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
|
||||
Schema::table('market_data', function (Blueprint $table) {
|
||||
$table->dropColumn('currency');
|
||||
$table->dropColumn('market_value_base');
|
||||
});
|
||||
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->dropColumn('cost_basis_base');
|
||||
$table->dropColumn('sale_price_base');
|
||||
});
|
||||
|
||||
Schema::table('dividends', function (Blueprint $table) {
|
||||
$table->dropColumn('dividend_amount_base');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('currencies');
|
||||
|
||||
Schema::dropIfExists('currency_rates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Currency;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CurrencySeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
||||
Currency::insert([
|
||||
['currency' => 'AUD', 'label' => 'Australian Dollar', 'created_at' => now()],
|
||||
['currency' => 'BRL', 'label' => 'Brazilian Real', 'created_at' => now()],
|
||||
['currency' => 'GBP', 'label' => 'British Pound', 'created_at' => now()],
|
||||
['currency' => 'CAD', 'label' => 'Canadian Dollar', 'created_at' => now()],
|
||||
['currency' => 'CNY', 'label' => 'Chinese Yuan', 'created_at' => now()],
|
||||
['currency' => 'CZK', 'label' => 'Czech Koruna', 'created_at' => now()],
|
||||
['currency' => 'DKK', 'label' => 'Danish Krone', 'created_at' => now()],
|
||||
['currency' => 'EUR', 'label' => 'Euro', 'created_at' => now()],
|
||||
['currency' => 'HKD', 'label' => 'Hong Kong Dollar', 'created_at' => now()],
|
||||
['currency' => 'INR', 'label' => 'Indian Rupee', 'created_at' => now()],
|
||||
['currency' => 'JPY', 'label' => 'Japanese Yen', 'created_at' => now()],
|
||||
['currency' => 'NZD', 'label' => 'New Zealand Dollar', 'created_at' => now()],
|
||||
['currency' => 'NOK', 'label' => 'Norwegian Krone', 'created_at' => now()],
|
||||
['currency' => 'SGD', 'label' => 'Singapore Dollar', 'created_at' => now()],
|
||||
['currency' => 'KRW', 'label' => 'South Korean Won', 'created_at' => now()],
|
||||
['currency' => 'ZAR', 'label' => 'South African Rand', 'created_at' => now()],
|
||||
['currency' => 'SEK', 'label' => 'Swedish Krona', 'created_at' => now()],
|
||||
['currency' => 'CHF', 'label' => 'Swiss Franc', 'created_at' => now()],
|
||||
['currency' => 'USD', 'label' => 'United States Dollar', 'created_at' => now()],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
|
||||
@@ -41,22 +41,21 @@ class MarketDataSeeder extends Seeder
|
||||
|
||||
$data = array_combine($header, $row);
|
||||
|
||||
$meta_data = json_decode(base64_decode($data['meta_data']), true);
|
||||
$meta_data['source'] = 'market_data_seeder';
|
||||
|
||||
$this->rows[] = [
|
||||
'symbol' => $data['symbol'],
|
||||
'name' => $data['name'],
|
||||
'meta_data' => json_encode([
|
||||
'country' => $data['country'],
|
||||
'first_trade_year' => $data['first_trade_year'],
|
||||
'sector' => $data['sector'],
|
||||
'industry' => $data['industry'],
|
||||
]),
|
||||
'currency' => $data['currency'],
|
||||
'meta_data' => json_encode($meta_data),
|
||||
];
|
||||
|
||||
$rowCount++;
|
||||
|
||||
if ($rowCount % $chunkSize == 0) {
|
||||
|
||||
$this->bulkInsert($this->rows);
|
||||
DB::table('market_data')->upsert($this->rows, ['symbol'], ['name', 'currency', 'meta_data']);
|
||||
$this->rows = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14465
-34981
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user