costLines([ $this->row('platform', '955859844397', '3767501840732782900', 20, 2800), $this->row('internal', 'DSG005016N11V01', '3767501840732782900', 20, 2800), ]); $this->assertSame(1, $placeholders); $this->assertSame(0, $bundles); $this->assertSame('DSG005016N11V01', $lines[0]['sku']); $this->assertSame(20.0, $lines[0]['qty']); } public function test_bundle_parent_is_removed_but_component_remains(): void { [$lines, $placeholders, $bundles] = $this->costLines([ $this->row('bundle-parent', '运营组合套装12994', '3832980288011632985', 2, 696), $this->row( 'bundle-child', 'SJ038017N1410V02', '3832980288011632985', 2, 625.26, '运营组合套装12994', ), ]); $this->assertSame(0, $placeholders); $this->assertSame(1, $bundles); $this->assertSame('SJ038017N1410V02', $lines[0]['sku']); $this->assertSame(2.0, $lines[0]['qty']); } public function test_plus_parent_is_removed_when_component_amounts_match(): void { [$lines, $placeholders, $bundles, $plusParents] = $this->costLines([ $this->row('plus-parent', 'GBGJ-A+V01+GBGJ-B+V01', '993830851777', 1, 648.06), $this->row('plus-child-a', 'GBGJ-AV01', '993830851777', 1, 450.03), $this->row('plus-child-b', 'GBGJ-BV01', '993830851777', 1, 198.03), ]); $this->assertSame(0, $placeholders); $this->assertSame(0, $bundles); $this->assertSame(1, $plusParents); $this->assertSame(['GBGJ-AV01', 'GBGJ-BV01'], array_column($lines, 'sku')); } public function test_plus_parent_remains_when_component_amounts_do_not_match(): void { [$lines, $placeholders, $bundles, $plusParents] = $this->costLines([ $this->row('plus-parent', 'GBGJ-A+V01+GBGJ-B+V01', '993830851777', 1, 1142), $this->row('plus-child-a', 'GBGJ-AV01', '993830851777', 1, 255.20), $this->row('plus-child-b', 'GBGJ-BV01', '993830851777', 1, 328.56), ]); $this->assertSame(0, $placeholders); $this->assertSame(0, $bundles); $this->assertSame(0, $plusParents); $this->assertContains('GBGJ-A+V01+GBGJ-B+V01', array_column($lines, 'sku')); } public function test_non_numeric_variant_parent_is_removed_when_quantity_and_amount_match(): void { [$lines, $placeholders, $bundles, $plusParents, $variantParents] = $this->costLines([ $this->row('variant-parent', 'TESTP001', '3767501840732782900', 1, 91), $this->row('variant-child', 'TESTP001N1150V01', '3767501840732782900', 1, 91), ]); $this->assertSame(0, $placeholders); $this->assertSame(0, $bundles); $this->assertSame(0, $plusParents); $this->assertSame(1, $variantParents); $this->assertSame(['TESTP001N1150V01'], array_column($lines, 'sku')); } public function test_non_numeric_variant_parent_remains_when_amounts_do_not_match(): void { [$lines, , , , $variantParents] = $this->costLines([ $this->row('variant-parent', 'TESTP001', '3767501840732782900', 1, 91), $this->row('variant-child', 'TESTP001N1150V01', '3767501840732782900', 1, 90), ]); $this->assertSame(0, $variantParents); $this->assertContains('TESTP001', array_column($lines, 'sku')); $this->assertContains('TESTP001N1150V01', array_column($lines, 'sku')); } public function test_non_numeric_variant_parent_does_not_cross_platform_item_groups(): void { [$lines, , , , $variantParents] = $this->costLines([ $this->row('variant-parent', 'TESTP001', '3767501840732782900', 1, 91), $this->row('variant-child', 'TESTP001N1150V01', '3767501840732782901', 1, 91), ]); $this->assertSame(0, $variantParents); $this->assertContains('TESTP001', array_column($lines, 'sku')); $this->assertContains('TESTP001N1150V01', array_column($lines, 'sku')); } public function test_unique_platform_sku_is_replaced_after_line_filtering(): void { [$lines] = $this->costLines([ $this->row( 'platform', '958530694526', '3768947181063635112', 1, 93.25, ), ]); $mapper = new StoreReceiptPlatformSkuMapper([ [ 'shop_sku_id' => '3533103677164802', 'shop_i_id' => '3768947181063635112', 'sku_id' => 'SG006367N1028V01', 'i_id' => 'SG0063', 'properties_value' => '经典北美黑胡桃色', ], ]); $command = new SyncStoreReceiptProductCost(); $method = new ReflectionMethod($command, 'mapPlatformSkus'); $method->setAccessible(true); $result = $method->invoke($command, $lines, $mapper); $this->assertSame(1, $result['auto_replaced_rows']); $this->assertSame('SG006367N1028V01', $result['lines'][0]['sku']); $this->assertSame('958530694526', $result['lines'][0]['original_platform_sku']); $this->assertSame('unique', $result['lines'][0]['platform_mapping_status']); $this->assertSame(0, $result['afs_replaced_rows']); } public function test_afs_mapping_is_used_when_history_has_no_internal_sku(): void { [$lines] = $this->costLines([ $this->row('platform', '905560897216', '3779397502214734671', 1, 297.52), ]); $command = new SyncStoreReceiptProductCost(); $method = new ReflectionMethod($command, 'mapPlatformSkus'); $method->setAccessible(true); $afsMappings = [ "抖音-佰汇家具店\0" . "3533103677164802\0" . '3779397502214734671' => [ 'SG009135N1063V01', ], ]; $result = $method->invoke( $command, $lines, new StoreReceiptPlatformSkuMapper([]), $afsMappings, '抖音-佰汇家具店', ); $this->assertSame(1, $result['auto_replaced_rows']); $this->assertSame(1, $result['afs_replaced_rows']); $this->assertSame('SG009135N1063V01', $result['lines'][0]['sku']); $this->assertSame('unique_afs', $result['lines'][0]['platform_mapping_status']); $this->assertSame('afs', $result['lines'][0]['platform_mapping_source']); } public function test_afs_multiple_candidates_remain_pending_and_history_ambiguity_is_not_overridden(): void { [$lines] = $this->costLines([ $this->row('platform', '905560897216', '3779397502214734671', 1, 297.52), ]); $command = new SyncStoreReceiptProductCost(); $method = new ReflectionMethod($command, 'mapPlatformSkus'); $method->setAccessible(true); $key = "抖音-佰汇家具店\0" . "3533103677164802\0" . '3779397502214734671'; $ambiguousAfs = $method->invoke( $command, $lines, new StoreReceiptPlatformSkuMapper([]), [$key => ['SG009105N1063V01', 'SG009135N1063V01']], '抖音-佰汇家具店', ); $this->assertSame(0, $ambiguousAfs['auto_replaced_rows']); $this->assertSame(1, $ambiguousAfs['ambiguous_rows']); $this->assertSame('905560897216', $ambiguousAfs['lines'][0]['sku']); $this->assertSame('ambiguous_afs', $ambiguousAfs['lines'][0]['platform_mapping_status']); $history = new StoreReceiptPlatformSkuMapper([ [ 'shop_sku_id' => '3533103677164802', 'shop_i_id' => '3779397502214734671', 'sku_id' => 'SG009105N1063V01', 'i_id' => 'SG0091', 'properties_value' => '', ], [ 'shop_sku_id' => '3533103677164802', 'shop_i_id' => '3779397502214734671', 'sku_id' => 'SG009135N1063V01', 'i_id' => 'SG0091', 'properties_value' => '', ], ]); $historyWins = $method->invoke( $command, $lines, $history, [$key => ['SG009135N1063V01']], '抖音-佰汇家具店', ); $this->assertSame(0, $historyWins['auto_replaced_rows']); $this->assertSame('ambiguous', $historyWins['lines'][0]['platform_mapping_status']); } public function test_yunfei_and_bcj_are_excluded_globally_and_from_bundle_components(): void { $command = new SyncStoreReceiptProductCost(); $constant = (new \ReflectionClass($command))->getReflectionConstant('EXCLUDED_SKUS'); $excludedSkus = $constant->getValue(); $this->assertContains('YUNFEI', $excludedSkus); $this->assertContains('BCJ', $excludedSkus); $method = new ReflectionMethod($command, 'expandComboSku'); $method->setAccessible(true); $components = $method->invoke( null, 'MKH-YUNFEI-1-BCJ-1-SG006367N1028V01-2', 3.0, ); $this->assertSame([['SG006367N1028V01', 6.0, true]], $components); } public function test_priced_line_always_calculates_cost_without_unit_mismatch_override(): void { $command = new SyncStoreReceiptProductCost(); $method = new ReflectionMethod($command, 'calculateLineCost'); $method->setAccessible(true); $result = $method->invoke($command, 9524.0, 58.0); $this->assertSame(9524.0, $result['quantity']); $this->assertSame(58.0, $result['unit_price']); $this->assertSame(552392.0, $result['cost']); } /** @return array{0:list>,1:int,2:int,3:int,4:int} */ private function costLines(array $rows): array { $command = new SyncStoreReceiptProductCost(); $method = new ReflectionMethod($command, 'costLines'); $method->setAccessible(true); $placeholders = 0; $bundles = 0; $plusParents = 0; $variantParents = 0; $lines = $method->invokeArgs($command, [$rows, &$placeholders, &$bundles, &$plusParents, &$variantParents]); return [$lines, $placeholders, $bundles, $plusParents, $variantParents]; } /** @return array */ private function row( string $rowKey, string $sku, string $shopItemId, float $quantity, float $amount, string $bundleParent = '', ): array { return [ 'source' => 'jst', 'sku' => $sku, 'qty' => $quantity, 'amount' => $amount, 'warehouse' => '简尚-新苏北仓', 'o_id' => '8986587', 'parent_o_id' => '', 'row_key' => $rowKey, 'status' => 'Sent', 'shop_i_id' => $shopItemId, 'shop_sku_id' => '3533103677164802', 'outer_oi_id' => 'OUTER-TEST-1', 'properties_value' => '颜色分类:六格-90*30*75cm-木纹胡桃色?赠送后背板', 'src_combine_sku_id' => $bundleParent, ]; } }