239 lines
12 KiB
PHP
239 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
|
|
use Modules\EcommerceFinance\Repositories\MarketingAccountBillMirrorRepository;
|
|
use Modules\EcommerceFinance\Services\AlibabaAccountBalanceWorkbookImporter;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|
use Tests\TestCase;
|
|
|
|
class AlibabaAccountBalanceRepositoryTest extends TestCase
|
|
{
|
|
private string $workbookPath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'database.connections.business_mirror' => [
|
|
'driver' => 'sqlite',
|
|
'database' => ':memory:',
|
|
'prefix' => '',
|
|
],
|
|
'upstream_mirrors.connection' => 'business_mirror',
|
|
]);
|
|
DB::purge('business_mirror');
|
|
$this->createTable();
|
|
$this->createBillTable();
|
|
$billMirror = \Mockery::mock(MarketingAccountBillMirrorRepository::class);
|
|
$billMirror->shouldReceive('ready')
|
|
->with(MarketingAccountBillMirrorRepository::ALIBABA)
|
|
->andReturnTrue();
|
|
$this->app->instance(MarketingAccountBillMirrorRepository::class, $billMirror);
|
|
CarbonImmutable::setTestNow('2026-09-11 12:00:00');
|
|
$this->workbookPath = sys_get_temp_dir() . '/alibaba-account-balance-' . uniqid('', true) . '.xlsx';
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (isset($this->workbookPath) && is_file($this->workbookPath)) {
|
|
unlink($this->workbookPath);
|
|
}
|
|
|
|
CarbonImmutable::setTestNow();
|
|
DB::purge('business_mirror');
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testItImportsUpdatesFiltersAndPaginatesBalanceSnapshots(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-30', '阿里店甲', null, null, null, 4142.96, null, null, null, null, null],
|
|
['2026-06-30', '阿里店乙', 100, 50, 20, 130, 130, 0, '', '已核对', '测试'],
|
|
]);
|
|
|
|
$importer = app(AlibabaAccountBalanceWorkbookImporter::class);
|
|
$first = $importer->import($this->workbookPath, '阿里巴巴余额.xlsx');
|
|
$second = $importer->import($this->workbookPath, '阿里巴巴余额.xlsx');
|
|
|
|
$this->assertSame(['created' => 2, 'updated' => 0, 'skipped' => 0, 'total' => 2], $first);
|
|
$this->assertSame(['created' => 0, 'updated' => 2, 'skipped' => 0, 'total' => 2], $second);
|
|
$this->assertSame(2, DB::connection('business_mirror')->table(AlibabaAccountBalanceRepository::TABLE)->count());
|
|
|
|
$repository = app(AlibabaAccountBalanceRepository::class);
|
|
$result = $repository->paginateMonthlyBalances('阿里店甲', '2026-06', 1, 25);
|
|
|
|
$this->assertTrue($repository->usesBusinessMirror());
|
|
$this->assertSame(['阿里店乙', '阿里店甲'], $repository->storeOptions());
|
|
$this->assertSame(1, $result['total']);
|
|
$this->assertSame('2026-06-30', $result['rows'][0]['date']);
|
|
$this->assertSame('阿里店甲', $result['rows'][0]['store_name']);
|
|
$this->assertSame(4142.96, $result['rows'][0]['closing_balance']);
|
|
$this->assertNull($result['rows'][0]['opening_balance']);
|
|
}
|
|
|
|
public function testItRejectsNonMonthEndDatesBeforeWriting(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-29', '阿里店甲', null, null, null, 10, null, null, null, null, null],
|
|
]);
|
|
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('必须是月末日期');
|
|
|
|
app(AlibabaAccountBalanceWorkbookImporter::class)->import($this->workbookPath);
|
|
}
|
|
|
|
public function testItCalculatesCompletedMonthsFromTheJuneSnapshotAndMonthlyBills(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-30', '阿里店甲', null, null, null, 100, null, null, null, null, '历史值'],
|
|
]);
|
|
app(AlibabaAccountBalanceWorkbookImporter::class)->import($this->workbookPath);
|
|
DB::connection('business_mirror')->table('js_alibaba_bill')->insert([
|
|
['id' => 1, 'shopName' => '阿里店甲', 'tradeDay' => '2026-07-05', 'tradeAMTIn' => 30, 'tradeAMTOut' => 5],
|
|
['id' => 2, 'shopName' => '阿里店甲', 'tradeDay' => '2026-07-20', 'tradeAMTIn' => 20, 'tradeAMTOut' => 15],
|
|
['id' => 3, 'shopName' => '阿里店甲', 'tradeDay' => '2026-08-02', 'tradeAMTIn' => 10, 'tradeAMTOut' => 40],
|
|
]);
|
|
|
|
$repository = app(AlibabaAccountBalanceRepository::class);
|
|
$july = $repository->paginateMonthlyBalances('阿里店甲', '2026-07');
|
|
$august = $repository->paginateMonthlyBalances('阿里店甲', '2026-08');
|
|
|
|
$this->assertSame(100.0, $july['rows'][0]['opening_balance']);
|
|
$this->assertSame(50.0, $july['rows'][0]['income_total']);
|
|
$this->assertSame(20.0, $july['rows'][0]['expense_total']);
|
|
$this->assertSame(130.0, $july['rows'][0]['closing_balance']);
|
|
$this->assertSame(0.0, $july['rows'][0]['system_closing_balance']);
|
|
$this->assertSame(-130.0, $july['rows'][0]['balance_difference']);
|
|
$this->assertSame('待核对', $july['rows'][0]['verification_status']);
|
|
$this->assertSame('系统期末余额与计算期末余额相差 130.00 元', $july['rows'][0]['difference_reason']);
|
|
|
|
$this->assertSame('2026-08-31', $august['rows'][0]['date']);
|
|
$this->assertSame(130.0, $august['rows'][0]['opening_balance']);
|
|
$this->assertSame(100.0, $august['rows'][0]['closing_balance']);
|
|
$this->assertSame(-100.0, $august['rows'][0]['balance_difference']);
|
|
}
|
|
|
|
public function testItKeepsHistoricalRowsUnchangedAndCarriesAcrossMonthsWithoutTransactions(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-30', '阿里店乙', null, null, null, 88.88, null, null, '历史原因', '', '历史备注'],
|
|
]);
|
|
app(AlibabaAccountBalanceWorkbookImporter::class)->import($this->workbookPath);
|
|
DB::connection('business_mirror')->table('js_alibaba_bill')->insert([
|
|
['id' => 1, 'shopName' => '阿里店乙', 'tradeDay' => '2026-08-12', 'tradeAMTIn' => 11.12, 'tradeAMTOut' => 0],
|
|
]);
|
|
|
|
$repository = app(AlibabaAccountBalanceRepository::class);
|
|
$historical = $repository->paginateMonthlyBalances('阿里店乙', '2026-06');
|
|
$august = $repository->paginateMonthlyBalances('阿里店乙', '2026-08');
|
|
|
|
$this->assertNull($historical['rows'][0]['opening_balance']);
|
|
$this->assertSame(88.88, $historical['rows'][0]['closing_balance']);
|
|
$this->assertNull($historical['rows'][0]['system_closing_balance']);
|
|
$this->assertSame('历史原因', $historical['rows'][0]['difference_reason']);
|
|
$this->assertSame('历史备注', $historical['rows'][0]['notes']);
|
|
$this->assertSame(88.88, $august['rows'][0]['opening_balance']);
|
|
$this->assertSame(100.0, $august['rows'][0]['closing_balance']);
|
|
}
|
|
|
|
public function testItExplainsMissingOpeningBalancesAndMarksZeroDifferencesVerified(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-30', '有期初店铺', null, null, null, 10, null, null, null, null, null],
|
|
]);
|
|
app(AlibabaAccountBalanceWorkbookImporter::class)->import($this->workbookPath);
|
|
DB::connection('business_mirror')->table('js_alibaba_bill')->insert([
|
|
['id' => 1, 'shopName' => '无期初店铺', 'tradeDay' => '2026-07-10', 'tradeAMTIn' => 12, 'tradeAMTOut' => 2],
|
|
['id' => 2, 'shopName' => '有期初店铺', 'tradeDay' => '2026-07-10', 'tradeAMTIn' => 5, 'tradeAMTOut' => 15],
|
|
]);
|
|
|
|
$repository = app(AlibabaAccountBalanceRepository::class);
|
|
$missing = $repository->paginateMonthlyBalances('无期初店铺', '2026-07')['rows'][0];
|
|
$verified = $repository->paginateMonthlyBalances('有期初店铺', '2026-07')['rows'][0];
|
|
|
|
$this->assertNull($missing['opening_balance']);
|
|
$this->assertNull($missing['closing_balance']);
|
|
$this->assertNull($missing['balance_difference']);
|
|
$this->assertSame(0.0, $missing['system_closing_balance']);
|
|
$this->assertSame('缺少上月期末余额', $missing['difference_reason']);
|
|
$this->assertSame('待核对', $missing['verification_status']);
|
|
$this->assertSame(0.0, $verified['closing_balance']);
|
|
$this->assertSame(0.0, $verified['balance_difference']);
|
|
$this->assertSame('', $verified['difference_reason']);
|
|
$this->assertSame('已核对', $verified['verification_status']);
|
|
}
|
|
|
|
public function testItDoesNotExposeTheCurrentOrFutureMonth(): void
|
|
{
|
|
$this->writeWorkbook([
|
|
['2026-06-30', '阿里店甲', null, null, null, 100, null, null, null, null, null],
|
|
]);
|
|
app(AlibabaAccountBalanceWorkbookImporter::class)->import($this->workbookPath);
|
|
DB::connection('business_mirror')->table('js_alibaba_bill')->insert([
|
|
['id' => 1, 'shopName' => '阿里店甲', 'tradeDay' => '2026-09-01', 'tradeAMTIn' => 20, 'tradeAMTOut' => 0],
|
|
]);
|
|
|
|
$repository = app(AlibabaAccountBalanceRepository::class);
|
|
|
|
$this->assertSame(['rows' => [], 'total' => 0], $repository->paginateMonthlyBalances(null, '2026-09'));
|
|
$this->assertSame(['rows' => [], 'total' => 0], $repository->paginateMonthlyBalances(null, '2026-10'));
|
|
}
|
|
|
|
/** @param array<int,array<int,mixed>> $rows */
|
|
private function writeWorkbook(array $rows): void
|
|
{
|
|
$spreadsheet = new Spreadsheet();
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
$sheet->setTitle('阿里巴巴账户');
|
|
$sheet->fromArray([[
|
|
'日期', '店铺名称', '期初余额', '本期收入', '本期支出', '期末余额',
|
|
'系统期末余额', '余额差异', '差异原因', '核对状态', '备注',
|
|
], ...$rows]);
|
|
(new Xlsx($spreadsheet))->save($this->workbookPath);
|
|
$spreadsheet->disconnectWorksheets();
|
|
}
|
|
|
|
private function createTable(): void
|
|
{
|
|
Schema::connection('business_mirror')->create(AlibabaAccountBalanceRepository::TABLE, function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->date('balance_date');
|
|
$table->string('store_name');
|
|
$table->decimal('opening_balance', 20, 6)->nullable();
|
|
$table->decimal('income_total', 20, 6)->nullable();
|
|
$table->decimal('expense_total', 20, 6)->nullable();
|
|
$table->decimal('closing_balance', 20, 6)->nullable();
|
|
$table->decimal('system_closing_balance', 20, 6)->nullable();
|
|
$table->decimal('balance_difference', 20, 6)->nullable();
|
|
$table->text('difference_reason')->nullable();
|
|
$table->string('verification_status')->nullable();
|
|
$table->text('notes')->nullable();
|
|
$table->string('source_file_name');
|
|
$table->char('source_sha256', 64);
|
|
$table->dateTime('imported_at');
|
|
$table->timestamps();
|
|
$table->unique(['balance_date', 'store_name']);
|
|
});
|
|
}
|
|
|
|
private function createBillTable(): void
|
|
{
|
|
Schema::connection('business_mirror')->create('js_alibaba_bill', function (Blueprint $table): void {
|
|
$table->unsignedBigInteger('id')->primary();
|
|
$table->string('shopName')->nullable();
|
|
$table->dateTime('tradeDay')->nullable();
|
|
$table->decimal('tradeAMTIn', 20, 6)->nullable();
|
|
$table->decimal('tradeAMTOut', 20, 6)->nullable();
|
|
});
|
|
}
|
|
}
|