## 完成内容 - 提现管理展示所选余额日及前两天余额。 - 连续3个自然日余额一致且均严格高于预留标准时显示预警。 - 余额导入成功后自动生成或更新待处理事项,同一店铺不重复创建。 - 导出文件同步包含三天余额和预警状态。 ## 验证 - 新增预警测试:3 项通过,30 个断言。 - 提现管理导入导出测试:12 项通过,96 个断言。 - 管理页面回归:1 项通过,56 个断言。 - 余额仓库测试:11 项通过,46 个断言。 - 本地页面已验证:39 家店铺触发预警,待办事项生成 39 条。 ## 风险 - 无数据库结构迁移。 - 不改上游数据;仅在成功导入余额后读取共享镜像并写入待办。 Reviewed-on: #519 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
220 lines
9.1 KiB
PHP
220 lines
9.1 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Business\WithdrawalBalanceAlertSynchronizer;
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\StoreReceivingAccount;
|
||
use App\Models\Business\Todo;
|
||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class WithdrawalBalanceAlertTest extends FeatureTestCase
|
||
{
|
||
/** @var list<string> */
|
||
private array $temporaryFiles = [];
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
$this->user->attachPermission(
|
||
Permission::where('name', 'read-admin-panel')->firstOrFail(),
|
||
);
|
||
$this->withoutMiddleware(ThrottleRequests::class);
|
||
}
|
||
|
||
public function test_three_unchanged_balances_above_reserve_show_warning_and_create_one_todo(): void
|
||
{
|
||
Carbon::setTestNow('2026-10-24 10:00:00');
|
||
|
||
try {
|
||
$store = StoreReceivingAccount::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'store_platform' => '京东',
|
||
'erp_store_name' => '京东-连续余额测试店',
|
||
'shop_name' => '京东-连续余额测试店',
|
||
'store_entity_name' => '连续余额测试主体',
|
||
'store_id' => 'JD-ALERT-001',
|
||
]);
|
||
|
||
foreach (['2026-10-21', '2026-10-22', '2026-10-23'] as $date) {
|
||
$this->insertBalanceSnapshot($date, '京东-连续余额测试店', 8000);
|
||
}
|
||
$sync = app(WithdrawalBalanceAlertSynchronizer::class);
|
||
$firstSync = $sync->syncForDate((int) $this->company->id, '2026-10-23');
|
||
$secondSync = $sync->syncForDate((int) $this->company->id, '2026-10-23');
|
||
$this->assertSame(1, $firstSync['created']);
|
||
$this->assertSame(0, $firstSync['updated']);
|
||
$this->assertSame(0, $secondSync['created']);
|
||
$this->assertSame(0, $secondSync['updated']);
|
||
|
||
$url = route('business.pages.show', [
|
||
'company_id' => $this->company->id,
|
||
'section' => 'fund-management',
|
||
'page' => 'withdrawal-management',
|
||
'balance_date' => '2026-10-23',
|
||
'platform' => '京东',
|
||
]);
|
||
$this->loginAs()->get($url)
|
||
->assertOk()
|
||
->assertSeeText('10月21日店铺余额')
|
||
->assertSeeText('10月22日店铺余额')
|
||
->assertSeeText('10月23日店铺余额')
|
||
->assertSeeText('已显示余额 1 条')
|
||
->assertSeeText('连续3天未变动')
|
||
->assertSeeText('8,000.00');
|
||
|
||
$title = '提现余额连续3天未变动|京东|京东-连续余额测试店';
|
||
$todo = Todo::withoutGlobalScopes()->where('company_id', $this->company->id)->where('title', $title)->first();
|
||
$this->assertNotNull($todo);
|
||
$this->assertSame('提现余额预警', $todo->category);
|
||
$this->assertSame('高', $todo->priority);
|
||
$this->assertSame('待处理', $todo->status);
|
||
$this->assertStringContainsString('2026-10-21 至 2026-10-23', (string) $todo->description);
|
||
$this->assertStringContainsString((string) $store->store_entity_name, (string) $todo->description);
|
||
|
||
$this->get($url)->assertOk();
|
||
$this->assertSame(
|
||
1,
|
||
Todo::withoutGlobalScopes()
|
||
->where('company_id', $this->company->id)
|
||
->where('category', '提现余额预警')
|
||
->count(),
|
||
);
|
||
|
||
$this->get(route('business.pages.show', [
|
||
'company_id' => $this->company->id,
|
||
'section' => 'other',
|
||
'page' => 'todos',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText($title)
|
||
->assertSeeText('提现余额预警')
|
||
->assertSeeText('连续余额测试主体');
|
||
} finally {
|
||
Carbon::setTestNow();
|
||
}
|
||
}
|
||
|
||
public function test_alert_requires_three_matched_days_an_exact_unchanged_value_and_a_strict_excess(): void
|
||
{
|
||
Carbon::setTestNow('2026-10-24 10:00:00');
|
||
|
||
try {
|
||
foreach (['等于预留标准店', '余额发生变动店', '缺少一天店'] as $name) {
|
||
StoreReceivingAccount::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'store_platform' => '京东',
|
||
'erp_store_name' => '京东-' . $name,
|
||
'shop_name' => '京东-' . $name,
|
||
'store_entity_name' => '预警边界测试主体',
|
||
]);
|
||
}
|
||
|
||
foreach (['2026-10-21' => 5000, '2026-10-22' => 5000, '2026-10-23' => 5000] as $date => $balance) {
|
||
$this->insertBalanceSnapshot($date, '京东-等于预留标准店', $balance);
|
||
}
|
||
foreach (['2026-10-21' => 8000, '2026-10-22' => 8000, '2026-10-23' => 7999.99] as $date => $balance) {
|
||
$this->insertBalanceSnapshot($date, '京东-余额发生变动店', $balance);
|
||
}
|
||
foreach (['2026-10-22' => 8000, '2026-10-23' => 8000] as $date => $balance) {
|
||
$this->insertBalanceSnapshot($date, '京东-缺少一天店', $balance);
|
||
}
|
||
$sync = app(WithdrawalBalanceAlertSynchronizer::class)->syncForDate(
|
||
(int) $this->company->id,
|
||
'2026-10-23',
|
||
);
|
||
$this->assertSame(0, $sync['created']);
|
||
|
||
$this->loginAs()->get(route('business.pages.show', [
|
||
'company_id' => $this->company->id,
|
||
'section' => 'fund-management',
|
||
'page' => 'withdrawal-management',
|
||
'balance_date' => '2026-10-23',
|
||
'platform' => '京东',
|
||
]))->assertOk()->assertSeeText('未触发')->assertSeeText('数据不足');
|
||
|
||
$this->assertSame(0, DB::table('business_todos')->count());
|
||
} finally {
|
||
Carbon::setTestNow();
|
||
}
|
||
}
|
||
|
||
public function test_successful_balance_import_runs_alert_generation_automatically(): void
|
||
{
|
||
Carbon::setTestNow('2026-10-24 10:00:00');
|
||
|
||
try {
|
||
StoreReceivingAccount::query()->create([
|
||
'company_id' => $this->company->id,
|
||
'store_platform' => '京东',
|
||
'erp_store_name' => '京东-导入预警测试店',
|
||
'shop_name' => '京东-导入预警测试店',
|
||
'store_entity_name' => '导入预警测试主体',
|
||
]);
|
||
$this->insertBalanceSnapshot('2026-10-21', '京东-导入预警测试店', 7000);
|
||
$this->insertBalanceSnapshot('2026-10-22', '京东-导入预警测试店', 7000);
|
||
|
||
$path = sys_get_temp_dir() . '/jst-balance-alert-' . uniqid('', true) . '.json';
|
||
$this->temporaryFiles[] = $path;
|
||
file_put_contents($path, json_encode([
|
||
'fldHeads' => [
|
||
['title' => '店铺', 'fld' => '店铺'],
|
||
['title' => '平台实际期末余额', 'fld' => '平台实际期末余额'],
|
||
],
|
||
'dataList' => [
|
||
['店铺' => '京东-导入预警测试店', '平台实际期末余额' => 7000],
|
||
],
|
||
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
|
||
|
||
$this->artisan('business:import-jushuitan-shop-balances', [
|
||
'path' => $path,
|
||
'--balance-date' => '2026-10-23',
|
||
'--scope' => 'jd-wallet',
|
||
])->assertExitCode(0);
|
||
$this->assertDatabaseHas('business_todos', [
|
||
'company_id' => $this->company->id,
|
||
'title' => '提现余额连续3天未变动|京东|京东-导入预警测试店',
|
||
'category' => '提现余额预警',
|
||
'status' => '待处理',
|
||
]);
|
||
} finally {
|
||
Carbon::setTestNow();
|
||
}
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
foreach ($this->temporaryFiles as $path) {
|
||
if (is_file($path)) {
|
||
unlink($path);
|
||
}
|
||
}
|
||
|
||
parent::tearDown();
|
||
}
|
||
|
||
private function insertBalanceSnapshot(string $date, string $storeName, float $balance): void
|
||
{
|
||
DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([
|
||
'balance_date' => $date,
|
||
'source_scope' => 'jd-wallet',
|
||
'source_platform' => '京东',
|
||
'store_name' => $storeName,
|
||
'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName),
|
||
'platform_account_id' => 'fixture-' . $storeName,
|
||
'balance' => $balance,
|
||
'source_row_key' => hash('sha256', $date . '|' . $storeName),
|
||
'source_file_name' => 'fixture.xlsx',
|
||
'source_sha256' => hash('sha256', 'fixture-' . $date . '-' . $storeName),
|
||
'imported_at' => now(),
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
}
|
||
}
|