['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 $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)); } }