CaiWuXiTong/tests/Unit/EcommerceFinance/JushuitanCostPriceSnapshotDifferTest.php

114 lines
6.4 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\JushuitanCostPriceObservationRepository;
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Modules\EcommerceFinance\Services\JushuitanCostPriceSnapshotDiffer as Differ;
use Tests\TestCase;
class JushuitanCostPriceSnapshotDifferTest extends TestCase
{
private Differ $differ;
protected function setUp(): void
{
parent::setUp();
config(['database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
'upstream_mirrors.connection' => 'business_mirror', 'cache.default' => 'array']);
DB::purge('business_mirror');
Carbon::setTestNow('2026-10-20 12:00:00');
$schema = Schema::connection('business_mirror');
$schema->create(Differ::STATES, function (Blueprint $t): void {
$t->string('dataset_key')->primary();
foreach (['status', 'source_key', 'source_object', 'target_table', 'run_mode', 'generation', 'last_error_code'] as $c) $t->string($c)->nullable();
foreach (['cursor', 'high_watermark'] as $c) $t->json($c)->nullable();
foreach (['window_started_at', 'window_ends_at', 'initial_completed_at', 'last_attempted_at', 'last_succeeded_at'] as $c) $t->dateTime($c, 6)->nullable();
$t->timestamps();
});
$schema->create('ops_jst_sku', function (Blueprint $t): void {
$t->char('_mirror_key', 64)->primary(); $t->string('sku_type'); $t->string('sku_id');
$t->decimal('cost_price', 20, 4)->nullable(); $t->dateTime('modified')->nullable(); $t->dateTime('_mirror_synced_at', 6);
});
$schema->create(Differ::SNAPSHOTS, function (Blueprint $t): void {
$t->char('source_key', 64)->primary(); $t->string('sku_id'); $t->decimal('cost_price', 20, 4); $t->dateTime('captured_at', 6);
});
$schema->create(Differ::OBSERVATIONS, function (Blueprint $t): void {
$t->char('id', 64)->primary(); $t->char('source_key', 64); $t->string('sku_id');
$t->decimal('before_price', 20, 4); $t->decimal('after_price', 20, 4);
$t->dateTime('previous_observed_at', 6); $t->dateTime('observed_at', 6); $t->dateTime('source_modified_at', 6)->nullable();
});
DB::connection('business_mirror')->table(Differ::STATES)->insert([
'dataset_key' => ProductCatalogMirrorRepository::DATASET_KEY, 'status' => 'succeeded',
'initial_completed_at' => '2026-10-19 00:00:00', 'last_succeeded_at' => '2026-10-20 11:59:00',
]);
$this->differ = new Differ(new GenericBusinessMirrorRepository(app('db')), new BusinessMirrorDatasetRegistry());
}
protected function tearDown(): void
{
Carbon::setTestNow();
parent::tearDown();
}
public function test_baseline_is_resumable_and_does_not_invent_history_then_observes_at_capture_time(): void
{
$db = DB::connection('business_mirror');
$this->product('a', 'SKU-A', 12);
$this->product('b', 'SKU-B', 20);
$this->assertSame('running', $this->differ->sync(1, 1)['status']);
$db->table(Differ::STATES)->where('dataset_key', Differ::DATASET_KEY)->update(['status' => 'failed']);
$this->assertSame('succeeded', $this->differ->sync(1, 3)['status']);
$this->assertSame(2, $db->table(Differ::SNAPSHOTS)->count());
$this->assertSame(0, $db->table(Differ::OBSERVATIONS)->count());
Carbon::setTestNow('2026-10-20 12:10:00');
$db->table('ops_jst_sku')->where('_mirror_key', 'a')->update(['cost_price' => 15.1234, '_mirror_synced_at' => '2026-10-20 12:09:00']);
$result = $this->differ->sync(10, 2);
$this->assertSame(1, $result['changes']);
$event = $db->table(Differ::OBSERVATIONS)->first();
$this->assertSame('2026-10-20 12:10:00.000000', $event->observed_at);
$this->assertSame('2026-01-01 00:00:00', $event->source_modified_at);
$this->assertEquals(12, $event->before_price);
$this->assertEquals(15.1234, $event->after_price);
$this->assertSame(0, $this->differ->sync(10, 2)['changes']);
$this->assertSame(1, $db->table(Differ::OBSERVATIONS)->count());
$this->assertFalse(Schema::connection('business_mirror')->hasTable('jushuitan_cost_price_history_rows'));
$this->assertFalse(Schema::connection('business_mirror')->hasTable('jushuitan_cost_price_history_sync_states'));
$rows = [(object) ['_mirror_key' => 'a'], (object) ['_mirror_key' => 'b']];
(new JushuitanCostPriceObservationRepository(new GenericBusinessMirrorRepository(app('db'))))->attach($rows);
$this->assertCount(1, $rows[0]->jushuitan_cost_price_observations);
$this->assertCount(0, $rows[1]->jushuitan_cost_price_observations);
$this->assertStringContainsString('不是真实改价时间', $rows[0]->jushuitan_cost_price_observations[0]['remark']);
}
public function test_new_products_null_prices_and_same_sku_on_different_source_rows_do_not_invent_changes(): void
{
$this->product('a', 'SAME', 12);
$this->product('b', 'SAME', 25);
$this->product('c', 'NULL', null);
$this->differ->sync();
Carbon::setTestNow('2026-10-20 12:10:00');
$this->product('d', 'NEW', 99, '2026-10-20 12:09:00');
$this->assertSame(0, $this->differ->sync()['changes']);
$this->assertSame(3, DB::connection('business_mirror')->table(Differ::SNAPSHOTS)->count());
DB::connection('business_mirror')->table(Differ::STATES)->where('dataset_key', ProductCatalogMirrorRepository::DATASET_KEY)->update(['status' => 'running']);
$this->assertSame('source_not_ready', $this->differ->sync()['status']);
}
private function product(string $key, string $sku, ?float $price, string $at = '2026-10-20 11:50:00'): void
{
DB::connection('business_mirror')->table('ops_jst_sku')->insert([
'_mirror_key' => $key, 'sku_id' => $sku, 'sku_type' => 'normal', 'cost_price' => $price,
'modified' => '2026-01-01 00:00:00', '_mirror_synced_at' => $at,
]);
}
}