CaiWuXiTong/tests/Unit/EcommerceFinance/StoreReportPaidSalesIncrementalTest.php

126 lines
7.4 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\StoreReportUpstreamRepository;
use Modules\EcommerceFinance\Services\StoreReportMirrorSynchronizer;
use Modules\EcommerceFinance\Services\StoreReportPaidSalesChanges;
use Tests\TestCase;
class StoreReportPaidSalesIncrementalTest extends TestCase
{
private function setupSummary(): StoreReportMirrorSynchronizer
{
config(['database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']]);
DB::purge('business_mirror');
config(['cache.stores.upstream_shared' => ['driver' => 'array'], 'system_database.mode' => 'shared']);
$schema = Schema::connection('business_mirror');
foreach (['store_report_import_states', 'store_report_paid_sales_rows', 'store_receipt_order_match_rows', StoreReportPaidSalesChanges::TABLE] as $table) $schema->dropIfExists($table);
(require base_path('database/migrations/2026_09_01_000000_create_store_report_mirror_tables.php'))->up();
$schema->table('store_report_import_states', function (Blueprint $table): void {
$table->string('run_mode')->nullable();
$table->timestamp('initial_completed_at')->nullable();
});
(require base_path('database/migrations/2026_10_16_234000_create_store_report_paid_sales_rows.php'))->up();
$schema->create('store_receipt_order_match_rows', function (Blueprint $table): void {
$table->increments('id');
$table->string('platform');
$table->date('report_month');
$table->date('occurred_date');
$table->string('store_name');
foreach (['income_amount', 'discount_amount', 'offset_amount', 'default_compensation_amount', 'refund_amount'] as $column) $table->decimal($column, 20, 4)->nullable();
});
(require base_path('database/migrations/2026_10_19_171000_track_paid_sales_dirty_dates.php'))->up();
DB::connection('business_mirror')->table('store_receipt_order_match_rows')->insert([
['platform' => 'A', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 100, 'discount_amount' => -5, 'offset_amount' => 0, 'default_compensation_amount' => -7, 'refund_amount' => -10],
['platform' => 'B', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 20, 'discount_amount' => 0, 'offset_amount' => 0, 'default_compensation_amount' => 0, 'refund_amount' => 0],
['platform' => 'A', 'report_month' => '2026-09-01', 'occurred_date' => '2026-09-30', 'store_name' => '测试店', 'income_amount' => 40, 'discount_amount' => 0, 'offset_amount' => 0, 'default_compensation_amount' => 0, 'refund_amount' => 0],
]);
return new StoreReportMirrorSynchronizer(\Mockery::mock(StoreReportUpstreamRepository::class), app('db'));
}
public function test_amount_change_without_row_count_change_only_rebuilds_affected_days(): void
{
$sync = $this->setupSummary();
$db = DB::connection('business_mirror');
$this->assertSame('succeeded', $sync->syncPaidSales()['status']);
$untouchedId = $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-09-30')->value('id');
$changes = app(StoreReportPaidSalesChanges::class);
$changes->markPeriod('2026-10', 'A');
$db->table('store_receipt_order_match_rows')->where('id', 1)->update(['income_amount' => 120]);
$this->assertTrue($sync->syncPaidSales()['changed']);
$this->assertSame(118.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount'));
$this->assertSame($untouchedId, $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-09-30')->value('id'));
$db->enableQueryLog();
$this->assertFalse($sync->syncPaidSales()['changed']);
$this->assertSame([], collect($db->getQueryLog())->filter(fn ($query) => str_contains($query['query'], 'store_receipt_order_match_rows'))->all());
}
public function test_date_moves_and_deletions_clear_old_totals_and_include_other_platforms(): void
{
$sync = $this->setupSummary();
$db = DB::connection('business_mirror');
$sync->syncPaidSales();
$changes = app(StoreReportPaidSalesChanges::class);
$changes->markPeriod('2026-10', 'A');
$db->table('store_receipt_order_match_rows')->where('id', 1)->update(['occurred_date' => '2026-10-03']);
$changes->markPeriod('2026-10', 'A');
$sync->syncPaidSales();
$this->assertSame(20.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount'));
$this->assertSame(78.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-03')->value('paid_amount'));
$changes->markPeriod('2026-10', 'B');
$db->table('store_receipt_order_match_rows')->where('platform', 'B')->delete();
$sync->syncPaidSales();
$this->assertSame(0, $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->count());
$this->assertSame(118.0, (float) $db->table('store_report_paid_sales_rows')->sum('paid_amount'));
}
public function test_a_new_revision_is_not_lost_when_an_older_snapshot_is_acknowledged(): void
{
$this->setupSummary();
$changes = app(StoreReportPaidSalesChanges::class);
$changes->markDates(['2026-10-01']);
$old = $changes->pending();
$changes->markDates(['2026-10-01']);
$changes->acknowledge($old);
$this->assertCount(1, $changes->pending());
}
public function test_active_writer_defers_summary_and_other_writers_without_consuming_changes(): void
{
$sync = $this->setupSummary();
$sync->syncPaidSales();
$changes = app(StoreReportPaidSalesChanges::class);
$changes->markDates(['2026-10-01']);
$lock = Cache::store('upstream_shared')->lock('store-report:paid-sales', 3600);
$this->assertTrue($lock->get());
try {
$this->assertFalse($sync->syncPaidSales()['changed']);
$this->assertCount(1, $changes->pending());
$this->artisan('store-receipt:order-match:sync', ['month' => '2026-10'])->assertExitCode(75);
} finally {
$lock->release();
}
$this->assertTrue($sync->syncPaidSales()['changed']);
$this->assertSame([], $changes->pending());
}
public function test_aggregation_failure_keeps_previous_totals_and_dirty_dates(): void
{
$sync = $this->setupSummary();
$db = DB::connection('business_mirror');
$sync->syncPaidSales();
app(StoreReportPaidSalesChanges::class)->markDates(['2026-10-01']);
$db->listen(function ($query): void {
if (str_starts_with($query->sql, 'INSERT INTO store_report_paid_sales_rows')) throw new \RuntimeException('test_aggregate_failure');
});
$this->assertSame('failed', $sync->syncPaidSales()['status']);
$this->assertSame(98.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount'));
$this->assertCount(1, app(StoreReportPaidSalesChanges::class)->pending());
}
}