fixes multi currency tests
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
+4
-11
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user