支持从最新余额快照清空并重建财务台账店铺表。 PR #623 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
368 lines
15 KiB
PHP
368 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Business\StoreReceivingAccount;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Mockery;
|
|
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerShopDataSynchronizer;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerTransactionWriter;
|
|
use Modules\EcommerceFinance\View\BillingDetailsPageData;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class FinanceLedgerShopDataSynchronizerTest extends FeatureTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->user->attachPermission(
|
|
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
|
);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Carbon::setTestNow();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_shop_balance_uses_latest_balance_on_or_before_previous_night(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-24 09:00:00');
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_shops')->insert([
|
|
'shop_name' => '京东-余额测试店',
|
|
'platform' => '京东',
|
|
'account_short' => '余额测试账户',
|
|
'shop_balance' => 1,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$this->insertBalance($connection, '2026-10-22', '京东-余额测试店', 222.22);
|
|
$this->insertBalance($connection, '2026-10-23', '京东-余额测试店', 333.33);
|
|
$this->insertBalance($connection, '2026-10-24', '京东-余额测试店', 444.44);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)->syncShopBalances('2026-10-23');
|
|
|
|
$this->assertSame('2026-10-23', $result['date']);
|
|
$this->assertSame(1, $result['matched']);
|
|
$shop = $connection->table('fin_shops')->where('shop_name', '京东-余额测试店')->first();
|
|
$this->assertSame(333.33, (float) $shop->shop_balance);
|
|
$this->assertSame('2026-10-23', substr((string) $shop->shop_balance_date, 0, 10));
|
|
}
|
|
|
|
public function test_balance_sync_adds_confirmed_store_that_is_missing_from_shop_ledger(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-24 09:00:00');
|
|
StoreReceivingAccount::withoutGlobalScopes()->create([
|
|
'company_id' => $this->company->id,
|
|
'store_platform' => '抖音',
|
|
'store_name' => '抖音-待补充店铺',
|
|
'store_entity_name' => '补充主体有限公司',
|
|
'receiving_method' => '银行卡',
|
|
]);
|
|
$this->insertBalance(
|
|
DB::connection('business_mirror'),
|
|
'2026-10-23',
|
|
'抖音-待补充店铺',
|
|
765.43,
|
|
'头条放心购',
|
|
);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)->syncShopBalances('2026-10-23');
|
|
|
|
$this->assertSame(1, $result['added']);
|
|
$shop = DB::connection('business_mirror')->table('fin_shops')
|
|
->where('shop_name', '抖音-待补充店铺')
|
|
->first();
|
|
$this->assertNotNull($shop);
|
|
$this->assertSame('抖音小店', $shop->platform);
|
|
$this->assertSame('补充主体有限公司', $shop->company_name);
|
|
$this->assertNull($shop->account_short);
|
|
$this->assertSame(765.43, (float) $shop->shop_balance);
|
|
$this->assertSame('2026-10-23', substr((string) $shop->shop_balance_date, 0, 10));
|
|
}
|
|
|
|
public function test_balance_sync_adds_missing_store_even_when_receiving_account_is_unknown(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-24 09:00:00');
|
|
$this->insertBalance(
|
|
DB::connection('business_mirror'),
|
|
'2026-10-23',
|
|
'京喜-待确认店铺',
|
|
432.10,
|
|
'京喜',
|
|
);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)->syncShopBalances('2026-10-23');
|
|
|
|
$this->assertSame(1, $result['added']);
|
|
$shop = DB::connection('business_mirror')->table('fin_shops')
|
|
->where('shop_name', '京喜-待确认店铺')
|
|
->first();
|
|
$this->assertNotNull($shop);
|
|
$this->assertSame('京喜企业店', $shop->platform);
|
|
$this->assertSame('待确认', $shop->status);
|
|
$this->assertNull($shop->company_name);
|
|
$this->assertNull($shop->account_short);
|
|
$this->assertNull($shop->payout_account);
|
|
$this->assertSame(432.10, (float) $shop->shop_balance);
|
|
$this->assertSame('2026-10-23', substr((string) $shop->shop_balance_date, 0, 10));
|
|
$this->assertStringContainsString('待确认回款账户', (string) $shop->remark);
|
|
}
|
|
|
|
public function test_rebuild_clears_stale_shop_ledger_and_reloads_latest_snapshot(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-26 09:00:00');
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_shops')->insert([
|
|
'shop_name' => '历史脏数据店',
|
|
'platform' => '天猫',
|
|
'shop_balance' => 99,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
StoreReceivingAccount::withoutGlobalScopes()->create([
|
|
'company_id' => $this->company->id,
|
|
'store_platform' => '拼多多',
|
|
'store_name' => '拼多多-台账重建店',
|
|
'store_entity_name' => '台账重建主体有限公司',
|
|
'withdrawal_account_name' => '台账重建回款户',
|
|
'bank_card_last_four' => '7788',
|
|
'store_status' => '正常',
|
|
]);
|
|
$connection->table('fin_account_map')->insert([
|
|
'source' => 'shop',
|
|
'source_value' => '拼多多-台账重建店',
|
|
'account_short' => '重建账户',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$this->insertBalance(
|
|
$connection,
|
|
'2026-10-25',
|
|
'拼多多-台账重建店',
|
|
1588.66,
|
|
'拼多多',
|
|
);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)
|
|
->syncShopBalances('2026-10-25', true);
|
|
|
|
$this->assertSame(1, $result['rebuilt']);
|
|
$this->assertSame(1, $result['matched']);
|
|
$this->assertDatabaseMissing('fin_shops', ['shop_name' => '历史脏数据店'], 'business_mirror');
|
|
$shop = $connection->table('fin_shops')->first();
|
|
$this->assertSame('拼多多-台账重建店', $shop->shop_name);
|
|
$this->assertSame('拼多多', $shop->platform);
|
|
$this->assertSame('台账重建主体有限公司', $shop->company_name);
|
|
$this->assertSame('重建账户', $shop->account_short);
|
|
$this->assertSame('在营', $shop->status);
|
|
$this->assertSame('台账重建回款户 7788', $shop->payout_account);
|
|
$this->assertSame(1588.66, (float) $shop->shop_balance);
|
|
$this->assertSame('2026-10-25', substr((string) $shop->shop_balance_date, 0, 10));
|
|
}
|
|
|
|
public function test_rebuild_without_balance_snapshot_keeps_existing_shop_ledger(): void
|
|
{
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_shops')->insert([
|
|
'shop_name' => '保留店铺',
|
|
'platform' => '天猫',
|
|
'shop_balance' => 88,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)
|
|
->syncShopBalances('2026-10-25', true);
|
|
|
|
$this->assertSame(0, $result['rebuilt']);
|
|
$this->assertDatabaseHas('fin_shops', [
|
|
'shop_name' => '保留店铺',
|
|
'shop_balance' => 88,
|
|
], 'business_mirror');
|
|
}
|
|
|
|
public function test_withdrawal_rows_are_mapped_to_shop_account_income_and_are_idempotent(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-24 09:00:00');
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_shops')->insert([
|
|
'shop_name' => '京东-提现测试店',
|
|
'platform' => '京东',
|
|
'account_short' => '提现测试账户',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$this->bindWithdrawalRows([
|
|
[
|
|
'record_code' => 'JD-WITHDRAW-1',
|
|
'store_name' => '京东-提现测试店',
|
|
'platform' => '京东',
|
|
'bill_item' => '提现',
|
|
'finance_remark' => '提现',
|
|
'occurred_at' => '2026-10-23 20:00:00',
|
|
'income' => '0.00',
|
|
'expense' => '123.45',
|
|
],
|
|
], 2);
|
|
|
|
$synchronizer = app(FinanceLedgerShopDataSynchronizer::class);
|
|
$first = $synchronizer->sync('2026-10-23', '2026-10-23', false);
|
|
$second = $synchronizer->sync('2026-10-23', '2026-10-23', false);
|
|
|
|
$this->assertSame(1, $first['withdrawals']['written']);
|
|
$this->assertSame(1, $second['withdrawals']['written']);
|
|
$this->assertDatabaseCount('fin_transactions', 1, 'business_mirror');
|
|
$transaction = $connection->table('fin_transactions')->first();
|
|
$this->assertSame('shop_bill', $transaction->source);
|
|
$this->assertSame('提现测试账户', $transaction->account_short);
|
|
$this->assertSame('店铺提现', $transaction->process_type);
|
|
$this->assertSame(123.45, (float) $transaction->income);
|
|
$this->assertSame(0.0, (float) $transaction->expense);
|
|
}
|
|
|
|
public function test_withdrawal_is_skipped_when_same_cashflow_was_already_synced_from_upstream(): void
|
|
{
|
|
Carbon::setTestNow('2026-10-24 09:00:00');
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_shops')->insert([
|
|
'shop_name' => '京东-重复提现店',
|
|
'platform' => '京东',
|
|
'account_short' => '重复提现账户',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$connection->table('fin_transactions')->insert([
|
|
'txn_date' => '2026-10-23',
|
|
'account_short' => '重复提现账户',
|
|
'detail' => '上游已同步提现',
|
|
'income' => 88.00,
|
|
'expense' => 0,
|
|
'approval_no' => 'UPSTREAM-1',
|
|
'source' => 'shop',
|
|
'source_value' => '京东-重复提现店',
|
|
'process_type' => '店铺提现',
|
|
'row_key' => hash('sha256', 'upstream-duplicate'),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$this->bindWithdrawalRows([
|
|
[
|
|
'record_code' => 'JD-WITHDRAW-2',
|
|
'store_name' => '京东-重复提现店',
|
|
'platform' => '京东',
|
|
'bill_item' => '提现',
|
|
'finance_remark' => '提现',
|
|
'occurred_at' => '2026-10-23 21:00:00',
|
|
'income' => '0.00',
|
|
'expense' => '88.00',
|
|
],
|
|
]);
|
|
|
|
$result = app(FinanceLedgerShopDataSynchronizer::class)->sync('2026-10-23', '2026-10-23', false);
|
|
|
|
$this->assertSame(0, $result['withdrawals']['written']);
|
|
$this->assertSame(1, $result['withdrawals']['upstream_duplicates']);
|
|
$this->assertSame(0, $connection->table('fin_transactions')->where('source', 'shop_bill')->count());
|
|
}
|
|
|
|
public function test_full_upstream_replacement_preserves_generated_shop_withdrawals(): void
|
|
{
|
|
$connection = DB::connection('business_mirror');
|
|
$connection->table('fin_transactions')->insert([
|
|
'txn_date' => '2026-10-23',
|
|
'account_short' => '保留提现账户',
|
|
'detail' => '页面生成提现',
|
|
'income' => 50,
|
|
'expense' => 0,
|
|
'approval_no' => 'JD:KEEP-1',
|
|
'source' => FinanceLedgerTransactionWriter::SHOP_WITHDRAWAL_SOURCE,
|
|
'source_value' => '京东-保留提现店',
|
|
'process_type' => '店铺提现',
|
|
'row_key' => hash('sha256', 'keep-shop-withdrawal'),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
app(FinanceLedgerTransactionWriter::class)->replace([
|
|
[
|
|
'txn_date' => '2026-10-23',
|
|
'account_short' => '上游账户',
|
|
'detail' => '上游动账',
|
|
'income' => 10,
|
|
'expense' => 0,
|
|
'approval_no' => 'UPSTREAM-2',
|
|
'source' => 'dingtalk',
|
|
'source_value' => '上游来源',
|
|
'process_type' => '费用',
|
|
],
|
|
], false);
|
|
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'source' => FinanceLedgerTransactionWriter::SHOP_WITHDRAWAL_SOURCE,
|
|
'approval_no' => 'JD:KEEP-1',
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'source' => 'dingtalk',
|
|
'approval_no' => 'UPSTREAM-2',
|
|
], 'business_mirror');
|
|
}
|
|
|
|
private function insertBalance(
|
|
$connection,
|
|
string $date,
|
|
string $storeName,
|
|
float $balance,
|
|
string $platform = '京东',
|
|
): void
|
|
{
|
|
$connection->table(JushuitanShopBalanceRepository::TABLE)->insert([
|
|
'balance_date' => $date,
|
|
'source_scope' => 'all',
|
|
'source_platform' => $platform,
|
|
'store_name' => $storeName,
|
|
'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName),
|
|
'platform_account_id' => null,
|
|
'balance' => $balance,
|
|
'source_row_key' => hash('sha256', $date . '|' . $storeName . '|' . $balance),
|
|
'source_file_name' => 'fixture.xlsx',
|
|
'source_sha256' => hash('sha256', 'fixture'),
|
|
'imported_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
private function bindWithdrawalRows(array $rows, int $calls = 1): void
|
|
{
|
|
$billing = Mockery::mock(BillingDetailsPageData::class);
|
|
$billing->shouldReceive('exportFilters')
|
|
->times($calls)
|
|
->andReturnUsing(static fn (array $input): array => $input + ['remark' => ['提现']]);
|
|
$billing->shouldReceive('exportColumns')
|
|
->times($calls)
|
|
->andReturn([
|
|
'record_code' => ['记录编码', 'text'],
|
|
'store_name' => ['店铺', 'text'],
|
|
'finance_remark' => ['财务备注', 'text'],
|
|
'bill_item' => ['账单项目', 'tag'],
|
|
'occurred_at' => ['发生时间', 'text'],
|
|
'income' => ['收入金额', 'num'],
|
|
'expense' => ['支出金额', 'num'],
|
|
]);
|
|
$billing->shouldReceive('exportRows')
|
|
->times($calls)
|
|
->andReturnUsing(static function () use ($rows): \Generator {
|
|
yield from $rows;
|
|
});
|
|
|
|
app()->instance(BillingDetailsPageData::class, $billing);
|
|
}
|
|
}
|