126 lines
7.4 KiB
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());
|
|
}
|
|
}
|