fixes multi currency tests

This commit is contained in:
hackerESQ
2025-04-11 20:57:21 -05:00
parent 26e54fb357
commit 38a65f99c9
6 changed files with 71 additions and 44 deletions
@@ -7,6 +7,8 @@ namespace App\Actions;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use function Illuminate\Support\defer;
class EnsureDailyChangeIsSynced class EnsureDailyChangeIsSynced
{ {
public function __invoke(Model $model, callable $next) public function __invoke(Model $model, callable $next)
@@ -8,7 +8,7 @@ use App\Models\CurrencyRate;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Queue\Queueable;
class BatchInsertNewCurrencyRatesJob implements ShouldQueue class QueuedCurrencyRateInsertJob implements ShouldQueue
{ {
use Queueable; use Queueable;
@@ -17,12 +17,10 @@ class BatchInsertNewCurrencyRatesJob implements ShouldQueue
*/ */
public $tries = 3; public $tries = 3;
public int $chunk_size = 100;
public function __construct( public function __construct(
protected array $updates protected array $chunk
) { ) {
$this->updates = $updates; $this->chunk = $chunk;
} }
/** /**
@@ -31,11 +29,6 @@ class BatchInsertNewCurrencyRatesJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
$chunks = array_chunk($this->updates, $this->chunk_size); CurrencyRate::insertOrIgnore($this->chunk);
foreach ($chunks as $chunk) {
CurrencyRate::insertOrIgnore($chunk);
}
} }
} }
+48 -26
View File
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use App\Jobs\BatchInsertNewCurrencyRatesJob; use App\Jobs\QueuedCurrencyRateInsertJob;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -97,7 +98,7 @@ class CurrencyRate extends Model
}); });
// persist // persist
BatchInsertNewCurrencyRatesJob::dispatch($updates); self::chunkInsert($updates);
return new CurrencyRate(Arr::first($updates, fn ($update) => $update['currency'] == $currency) ?? ['rate' => 1]); return new CurrencyRate(Arr::first($updates, fn ($update) => $update['currency'] == $currency) ?? ['rate' => 1]);
}); });
@@ -148,38 +149,19 @@ class CurrencyRate extends Model
$updates = []; $updates = [];
foreach ($period as $date) { foreach ($period as $date) {
$skip = false; $lookupDate = self::getNearestPastDate($date, $rates);
$lookupDate = $date->toDateString(); if (is_null($lookupDate)) {
// get rates or find closest valid rate (handles missing weekend rates)
while (! isset($rates[$lookupDate])) {
$lookupDate = Carbon::parse($lookupDate)->subDay();
// prevent runaway infinite loops
if ($lookupDate->lessThan($date->copy()->subWeek())) {
$skip = true;
break;
}
$lookupDate = $lookupDate->toDateString();
}
if ($skip) {
continue; continue;
} }
// make date a string
$date = $date->toDateString();
// loop through each rate // loop through each rate
foreach ($rates[$lookupDate] as $curr => $rate) { foreach ($rates[$lookupDate->toDateString()] as $curr => $rate) {
// add to updates // add to updates
$updates[] = [ $updates[] = [
'currency' => $curr, 'currency' => $curr,
'date' => $date, 'date' => $date->toDateString(),
'rate' => $rate, 'rate' => $rate,
'updated_at' => now()->toDateTimeString(), 'updated_at' => now()->toDateTimeString(),
'created_at' => now()->toDateTimeString(), 'created_at' => now()->toDateTimeString(),
@@ -188,7 +170,7 @@ class CurrencyRate extends Model
} }
// persist // persist
BatchInsertNewCurrencyRatesJob::dispatch($updates); self::chunkInsert($updates);
return collect($updates) return collect($updates)
->whereBetween('date', [$start, $end ?? now()]) ->whereBetween('date', [$start, $end ?? now()])
@@ -199,6 +181,35 @@ class CurrencyRate extends Model
->toArray(); ->toArray();
} }
private static function getNearestPastDate(CarbonInterface $date, array $rates): ?CarbonInterface
{
$datesWithRates = array_keys($rates);
sort($datesWithRates);
// get rates or find closest valid rate (handles missing weekend rates)
while (! isset($rates[$date->toDateString()])) {
// is this the start of a range that falls on a weekend?
if ($date->lessThan($first_date = Carbon::parse($datesWithRates[0]))) {
$date = $first_date;
break;
}
// try the day before then
$date = Carbon::parse($date)->subDay();
// prevent runaway infinite loops
if ($date->lessThan($date->copy()->subWeek())) {
$date = null;
break;
}
}
return $date;
}
public static function refreshCurrencyData($force = false): void public static function refreshCurrencyData($force = false): void
{ {
$currencies = Currency::all()->pluck('currency')->toArray(); $currencies = Currency::all()->pluck('currency')->toArray();
@@ -234,6 +245,17 @@ class CurrencyRate extends Model
} }
} }
public static function chunkInsert(array $updates): void
{
$chunks = array_chunk($updates, 250);
foreach ($chunks as $chunk) {
QueuedCurrencyRateInsertJob::dispatch($chunk);
}
}
protected static function getCurrencyAliasAdjustments($currency) protected static function getCurrencyAliasAdjustments($currency)
{ {
$adjustment = 1; $adjustment = 1;
+8
View File
@@ -7,7 +7,9 @@ namespace Tests\Api;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\Transaction; use App\Models\Transaction;
use App\Models\User; use App\Models\User;
use Database\Seeders\CurrencySeeder;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase; use Tests\TestCase;
class TransactionsTest extends TestCase class TransactionsTest extends TestCase
@@ -69,6 +71,11 @@ class TransactionsTest extends TestCase
public function test_can_create_transaction() public function test_can_create_transaction()
{ {
Artisan::call('db:seed', [
'--class' => CurrencySeeder::class,
'--force' => true,
]);
$this->actingAs($this->user); $this->actingAs($this->user);
$data = [ $data = [
@@ -76,6 +83,7 @@ class TransactionsTest extends TestCase
'portfolio_id' => $this->portfolio->id, 'portfolio_id' => $this->portfolio->id,
'transaction_type' => 'BUY', 'transaction_type' => 'BUY',
'quantity' => 10, 'quantity' => 10,
'currency' => 'USD',
'date' => now()->toDateString(), 'date' => now()->toDateString(),
'cost_basis' => 150, 'cost_basis' => 150,
]; ];
+4 -2
View File
@@ -241,9 +241,11 @@ class DailyChangeTest extends TestCase
'transaction_type' => 'BUY', 'transaction_type' => 'BUY',
]); ]);
$daily_change_day_after = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->addDay())->first(); $daily_change_day_after = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->nextWeekday())->first();
$daily_change_day_before = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->subDay())->first(); $daily_change_day_before = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->previousWeekday())->first();
$this->assertNotNull($daily_change_day_after);
$this->assertNotNull($daily_change_day_before);
$this->assertEquals(39.89, $daily_change_day_after->total_cost_basis - $daily_change_day_before->total_cost_basis); $this->assertEquals(39.89, $daily_change_day_after->total_cost_basis - $daily_change_day_before->total_cost_basis);
} }
} }
+5 -5
View File
@@ -17,6 +17,7 @@ use App\Models\User;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Database\Seeders\CurrencySeeder; use Database\Seeders\CurrencySeeder;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Investbrain\Frankfurter\Frankfurter; use Investbrain\Frankfurter\Frankfurter;
use Mockery; use Mockery;
@@ -210,10 +211,10 @@ class MultiCurrencyTest extends TestCase
$start = now()->subWeeks(2); $start = now()->subWeeks(2);
$end = now(); $end = now();
$results = [];
$period = CarbonPeriod::create($start, $end); $period = CarbonPeriod::create($start, $end);
// mock response from Frankfurter
$results = [];
collect($period->copy()->filter('isWeekday'))->each(function ($date) use (&$results) { collect($period->copy()->filter('isWeekday'))->each(function ($date) use (&$results) {
$date = $date->toDateString(); $date = $date->toDateString();
@@ -299,7 +300,6 @@ class MultiCurrencyTest extends TestCase
public function test_can_handle_aliases_for_time_series_rates() public function test_can_handle_aliases_for_time_series_rates()
{ {
$start = now()->subWeeks(2); $start = now()->subWeeks(2);
$end = now(); $end = now();
$adjustment = 100; $adjustment = 100;
@@ -333,8 +333,8 @@ class MultiCurrencyTest extends TestCase
$result = CurrencyRate::timeSeriesRates('ZZZ', $start, $end); $result = CurrencyRate::timeSeriesRates('ZZZ', $start, $end);
$this->assertEquals( $this->assertEquals(
$results[$end->toDateString()]['YYY'] * $adjustment, Arr::last($results)['YYY'] * $adjustment,
$result[$end->toDateString()] Arr::last($result)
); );
} }