- 复用已登记镜像 jiansun.dbo.jst_workOrderInfo,不新增上游链路 - 口径与聚工单「记账」视图一致:工单类型“记账”,状态待审核/已拒绝/待处理/处理中/已完成, 记账原因为厂家记账 12 项 + 仓库记账 5 项二级原因,发起时间 >= 2026-09-01;同一工单只取最新一版 - 新页面 供应商应付 → 厂家记账:筛选、记账金额合计、列设置、CSV 导出;首次同步未完成时明确提示 - 镜像索引迁移 (woTypeId, created, Id) 涉及:金额展示(只读)、镜像索引迁移;不涉及权限模型和上游写入。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5KQeRJaLrbJY9iKNC5veV
166 lines
9.3 KiB
PHP
166 lines
9.3 KiB
PHP
<?php
|
||
|
||
namespace Tests\Unit\EcommerceFinance;
|
||
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Modules\EcommerceFinance\Repositories\SupplierBookkeepingWorkOrderRepository as Repository;
|
||
use Modules\EcommerceFinance\View\SupplierBookkeepingPageData;
|
||
use Tests\TestCase;
|
||
|
||
class SupplierBookkeepingWorkOrderRepositoryTest extends TestCase
|
||
{
|
||
private const REASON = Repository::FIELD_REASON;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
config([
|
||
'database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''],
|
||
'upstream_mirrors.connection' => 'business_mirror',
|
||
'upstream_mirrors.datasets' => [
|
||
Repository::DATASET => [
|
||
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_workOrderInfo',
|
||
'target' => 'fixture_work_orders', 'keys' => ['Id'], 'watermark' => 'updateTime',
|
||
'indexes' => [['woTypeId', 'woNo', 'Id']],
|
||
],
|
||
],
|
||
]);
|
||
Cache::setDefaultDriver('array');
|
||
Cache::flush();
|
||
DB::purge('business_mirror');
|
||
|
||
$schema = Schema::connection('business_mirror');
|
||
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
|
||
$table->string('dataset_key')->primary();
|
||
$table->string('status')->nullable();
|
||
$table->dateTime('initial_completed_at')->nullable();
|
||
$table->dateTime('last_succeeded_at')->nullable();
|
||
});
|
||
$schema->create('fixture_work_orders', function (Blueprint $table): void {
|
||
$table->integer('Id');
|
||
foreach (['woNo', 'woTypeId', 'status', 'createby', 'executors', 'created', 'updated', 'updateTime', 'businessFieldList'] as $column) {
|
||
$table->text($column)->nullable();
|
||
}
|
||
});
|
||
}
|
||
|
||
private function state(?string $initialCompletedAt = '2026-10-01 00:00:00', string $status = 'succeeded'): void
|
||
{
|
||
DB::connection('business_mirror')->table('business_mirror_table_states')->updateOrInsert(
|
||
['dataset_key' => Repository::DATASET],
|
||
['status' => $status, 'initial_completed_at' => $initialCompletedAt, 'last_succeeded_at' => '2026-10-18 21:00:00'],
|
||
);
|
||
}
|
||
|
||
/** @param array<string,mixed> $fields */
|
||
private function order(int $id, string $woNo, array $fields, array $patch = [], int $jsonFlags = JSON_UNESCAPED_UNICODE): void
|
||
{
|
||
$list = [];
|
||
foreach ($fields as $fieldId => $value) {
|
||
$list[] = is_array($value) && array_key_exists('__raw', $value)
|
||
? ['fieldId' => $fieldId] + $value['__raw']
|
||
: ['fieldId' => $fieldId, 'value' => $value, 'valueLabel' => null];
|
||
}
|
||
DB::connection('business_mirror')->table('fixture_work_orders')->insert(array_replace([
|
||
'Id' => $id, 'woNo' => $woNo, 'woTypeId' => Repository::WORK_TYPE_ID, 'status' => 'WAIT_CONFIRM',
|
||
'createby' => '发起人甲', 'executors' => '简尚', 'created' => '2026-09-05 10:00:00',
|
||
'updated' => '2026-09-06 10:00:00', 'updateTime' => '2026-09-06 10:00:00',
|
||
'businessFieldList' => json_encode($list, $jsonFlags),
|
||
], $patch));
|
||
}
|
||
|
||
private function repository(): Repository
|
||
{
|
||
return $this->app->make(Repository::class);
|
||
}
|
||
|
||
public function test_reports_pending_until_initial_sync_and_refuses_empty_reads(): void
|
||
{
|
||
$this->assertSame('unavailable', $this->repository()->syncStatus()['code']);
|
||
$this->state(null, 'running');
|
||
$this->assertSame('initializing', $this->repository()->syncStatus()['code']);
|
||
|
||
$this->expectExceptionMessage('business_mirror_dataset_not_ready');
|
||
$this->repository()->rows();
|
||
}
|
||
|
||
public function test_applies_confirmed_scope_latest_version_and_parses_fields(): void
|
||
{
|
||
$this->state();
|
||
$base = [
|
||
self::REASON => '厂家记账/板材爆口/掉漆/开裂/发霉/色差',
|
||
'SELECTField1413856620250906100658' => '补偿款项',
|
||
'NUMBERField1413856620250906100530' => '120.5',
|
||
'orderField' => '6918600558615604325',
|
||
'shopField' => ['__raw' => ['value' => 18533663, 'valueLabel' => '抖音-艺煦白墙专卖店']],
|
||
'attachmentField' => ['https://img.example/a.png', 'https://img.example/b.png'],
|
||
'itemInfo' => ['__raw' => ['value' => null, 'childFieldList' => [
|
||
[['fieldId' => 'skuId', 'value' => 'CBG007215N16V01'], ['fieldId' => 'iid', 'value' => 'CBG0072'], ['fieldId' => 'supplierName', 'value' => '闻其'], ['fieldId' => 'qty', 'value' => 1]],
|
||
[['fieldId' => 'skuId', 'value' => 'CBG007216N16V01'], ['fieldId' => 'iid', 'value' => 'CBG0072'], ['fieldId' => 'supplierName', 'value' => '闻其'], ['fieldId' => 'qty', 'value' => 2]],
|
||
]]],
|
||
];
|
||
|
||
// 同一工单两版:只取最新一版(金额被修改为 130)。
|
||
$this->order(1, 'GD-A', $base);
|
||
$this->order(9, 'GD-A', array_replace($base, ['NUMBERField1413856620250906100530' => '130']), ['status' => 'CONFIRMING']);
|
||
// 仓库记账:数组形式的级联值 + \u 转义 JSON,API 形式的商品明细。
|
||
$this->order(2, 'GD-B', [
|
||
self::REASON => ['仓库记账', '退货运费'],
|
||
'NUMBERField1413856620250906100530' => '10.05',
|
||
'itemInfo' => [['skuId' => 'SKU-B', 'supplierName' => '聚尚', 'priceCount' => 99.5]],
|
||
], ['created' => '2026-09-01 00:00:00'], 0);
|
||
// 以下均不应接入。
|
||
$this->order(3, 'GD-C', [self::REASON => '买家原因/原单退回(导致退货,退回财务改记账)']);
|
||
$this->order(4, 'GD-D', $base, ['created' => '2026-08-31 23:59:59']);
|
||
$this->order(5, 'GD-E', $base);
|
||
$this->order(6, 'GD-E', $base, ['status' => 'CANCELED']);
|
||
$this->order(7, 'GD-F', $base, ['woTypeId' => 'OTHER']);
|
||
$this->order(8, 'GD-G', [self::REASON => '厂家记账/未勾选的新原因']);
|
||
$this->order(12, 'GD-I', $base, ['status' => '已取消']);
|
||
$this->order(13, 'GD-J', $base, ['status' => '已完成', 'created' => '2026-09-02 00:00:00']);
|
||
$this->order(10, 'GD-H', $base);
|
||
$this->order(11, 'GD-H', array_replace($base, [self::REASON => '快递记账/原单退回']));
|
||
|
||
$rows = $this->repository()->rows();
|
||
|
||
$this->assertSame(['GD-A', 'GD-J', 'GD-B'], array_column($rows, 'work_order_no'));
|
||
[$a, $j, $b] = $rows;
|
||
$this->assertSame('已完成', $j['status']);
|
||
$this->assertSame('130.00', $a['amount']);
|
||
$this->assertSame('处理中', $a['status']);
|
||
$this->assertSame('厂家记账', $a['reason_level_1']);
|
||
$this->assertSame('板材爆口/掉漆/开裂/发霉/色差', $a['reason_level_2']);
|
||
$this->assertSame('补偿款项', $a['bookkeeping_type']);
|
||
$this->assertSame('抖音-艺煦白墙专卖店', $a['shop_name']);
|
||
$this->assertSame("CBG007215N16V01\nCBG007216N16V01", $a['product_code']);
|
||
$this->assertSame("1\n2", $a['product_quantity']);
|
||
$this->assertSame(2, $a['attachment_count']);
|
||
$this->assertSame('6918600558615604325', $a['online_order_no']);
|
||
$this->assertSame('仓库记账/退货运费', $b['bookkeeping_reason']);
|
||
$this->assertSame('聚尚', $b['supplier']);
|
||
$this->assertSame('99.5', $b['product_amount']);
|
||
$this->assertSame('待处理', $b['status']);
|
||
$this->assertCount(17, Repository::reasonPaths());
|
||
}
|
||
|
||
public function test_page_filters_and_totals_use_cents(): void
|
||
{
|
||
$rows = [
|
||
['started_at' => '2026-09-02 08:00:00', 'bookkeeping_reason' => '厂家记账/缺货损失', 'reason_level_1' => '厂家记账', 'status' => '待处理', 'bookkeeping_type' => '记账全款', 'amount' => '0.10', 'work_order_no' => 'GD-1', 'online_order_no' => '', 'tracking_no' => '', 'shop_name' => '天猫-亿家达旗舰店', 'supplier' => '彩虹', 'product_code' => 'DNZ01'],
|
||
['started_at' => '2026-09-20 08:00:00', 'bookkeeping_reason' => '仓库记账/退货运费', 'reason_level_1' => '仓库记账', 'status' => '已完成', 'bookkeeping_type' => '运费损耗', 'amount' => '0.20', 'work_order_no' => 'GD-2', 'online_order_no' => '', 'tracking_no' => '', 'shop_name' => '拼多多-宜自然', 'supplier' => '胜友', 'product_code' => 'XG01'],
|
||
];
|
||
$page = $this->app->make(SupplierBookkeepingPageData::class);
|
||
$filters = ['started_from' => '', 'started_to' => '', 'reason' => '', 'status' => '', 'bookkeeping_type' => ''] + array_fill_keys(array_keys(SupplierBookkeepingPageData::TEXT_FILTERS), '');
|
||
|
||
$all = iterator_to_array($page->rows($filters, $rows), false);
|
||
$this->assertSame(30, array_sum(array_map([SupplierBookkeepingPageData::class, 'cents'], array_column($all, 'amount'))));
|
||
$this->assertSame(['GD-1'], array_column(iterator_to_array($page->rows(['reason' => '厂家记账'] + $filters, $rows), false), 'work_order_no'));
|
||
$this->assertSame(['GD-2'], array_column(iterator_to_array($page->rows(['started_from' => '2026-09-10', 'supplier' => '胜'] + $filters, $rows), false), 'work_order_no'));
|
||
$this->assertSame([], iterator_to_array($page->rows(['started_to' => '2026-09-01'] + $filters, $rows), false));
|
||
}
|
||
}
|