'879735', 'date' => '2026-09-10', 'skuCode' => 'DNZ033609', 'productCode' => 'DNZ033609N1178V01', 'unitPrice' => '100', 'qty' => '20', 'amount' => '2000', ]]; $theirs = [[ 'purchaseNo' => '879735', 'createdAt' => '2026-09-17', 'barcode' => 'DNZ033609N1097V01', 'unitPrice' => '100', 'qty' => '20', 'amount' => '2000', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(0, $result['summary']['mismatch']); $this->assertSame(0.0, $result['summary']['differenceAmount']); $this->assertSame(2000.0, $result['summary']['settleAmount']); $this->assertSame(7, $result['rows'][0]['dateToleranceDays']); $this->assertSame(2, $result['rows'][0]['matchedByLayer']); $this->assertStringContainsString('已忽略日期差异', $result['rows'][0]['note']); } public function test_purchase_and_barcode_match_has_no_seven_day_limit(): void { $mine = [[ 'purchaseNo' => '879735', 'date' => '2026-09-10', 'skuCode' => 'DNZ033609', 'qty' => '20', 'amount' => '2000', ]]; $theirs = [[ 'purchaseNo' => '879735', 'createdAt' => '2026-09-18', 'barcode' => 'DNZ033609N1097V01', 'qty' => '20', 'amount' => '2000', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(8, $result['rows'][0]['dateDifferenceDays']); } public function test_purchase_and_barcode_match_then_reports_quantity_and_amount_difference(): void { $mine = [[ 'purchaseNo' => '879735', 'date' => '2026-09-10', 'skuCode' => 'DNZ033609', 'qty' => '20', 'amount' => '2000', ]]; $theirs = [[ 'purchaseNo' => '879735', 'createdAt' => '2026-09-12', 'barcode' => 'DNZ033609N1097V01', 'qty' => '19', 'amount' => '1900', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(0, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(1, $result['summary']['mismatch']); $this->assertSame(-1.0, $result['rows'][0]['qtyDifference']); $this->assertSame(-100.0, $result['rows'][0]['amountDifference']); $this->assertStringContainsString('采购单号+商品编码匹配', $result['rows'][0]['note']); } public function test_purchase_and_barcode_match_with_unrecognized_supplier_date(): void { $mine = [[ 'purchaseNo' => '879735', 'date' => '2026-10-10', 'skuCode' => 'SJ009363', 'productCode' => 'SJ009363N160V01', 'qty' => '130', 'amount' => '12350', ]]; $theirs = [[ 'purchaseNo' => '879735', 'createdAt' => '2026.1', 'barcode' => 'SJ009363N160V01', 'qty' => '130', 'amount' => '12350', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(0, $result['summary']['mismatch']); $this->assertSame('2026-10-10', $result['rows'][0]['date']); $this->assertSame(2, $result['rows'][0]['matchedByLayer']); $this->assertStringContainsString('已忽略日期差异', $result['rows'][0]['note']); } public function test_purchase_and_barcode_aggregate_then_report_ambiguous_quantity(): void { $mine = [ [ 'purchaseNo' => '879735', 'date' => '2026-10-10', 'skuCode' => 'SJ009363', 'qty' => '130', 'amount' => '12350', ], [ 'purchaseNo' => '879735', 'date' => '2026-10-12', 'skuCode' => 'SJ009363', 'qty' => '130', 'amount' => '12350', ], ]; $theirs = [[ 'purchaseNo' => '879735', 'createdAt' => '2026.1', 'barcode' => 'SJ009363N160V01', 'qty' => '130', 'amount' => '12350', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(0, $result['summary']['match']); $this->assertSame(1, $result['summary']['mismatch']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(260.0, $result['rows'][0]['mineQty']); $this->assertSame(130.0, $result['rows'][0]['theirsQty']); } public function test_purchase_and_barcode_match_aggregates_mine_rows_across_dates(): void { $mine = [ [ 'purchaseNo' => '923923', 'date' => '2026-10-03', 'skuCode' => 'CBG011611', 'qty' => '36', 'amount' => '2466', ], [ 'purchaseNo' => '923923', 'date' => '2026-10-04', 'skuCode' => 'CBG011611', 'qty' => '44', 'amount' => '3014', ], ]; $theirs = [[ 'purchaseNo' => '923923', 'createdAt' => '2026.1', 'barcode' => 'CBG011611N160V01', 'qty' => '80', 'amount' => '5480', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(80.0, $result['rows'][0]['mineQty']); $this->assertSame(5480.0, $result['rows'][0]['mineAmount']); $this->assertSame(2, $result['rows'][0]['dateCount']); $this->assertSame(2, $result['rows'][0]['matchedByLayer']); $this->assertStringContainsString('已忽略日期差异', $result['rows'][0]['note']); } public function test_same_amount_rows_with_different_purchase_numbers_are_all_retained(): void { $mine = [ [ 'purchaseNo' => '911299', 'date' => '2026-10-10', 'skuCode' => 'XG007322', 'productCode' => 'XG007322N160V01', 'qty' => '30', 'amount' => '4530', ], [ 'purchaseNo' => '941858', 'date' => '2026-10-10', 'skuCode' => 'XG007322', 'productCode' => 'XG007322N160V01', 'qty' => '30', 'amount' => '4530', ], ]; $theirs = [[ 'purchaseNo' => '941858', 'createdAt' => '2026.1', 'barcode' => 'XG007322N160V01', 'qty' => '30', 'amount' => '4530', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(1, $result['summary']['supplierMissing']); $this->assertSame(9060.0, $result['summary']['mineAmount']); $this->assertSame(4530.0, $result['summary']['theirsAmount']); $this->assertSame(4530.0, $result['summary']['differenceAmount']); $this->assertCount(2, $result['rows']); $this->assertSame('911299', $result['rows'][0]['purchaseNo']); $this->assertSame(SupplierStatementMatcher::TYPE_SUPPLIER_MISSING, $result['rows'][0]['type']); $this->assertSame('941858', $result['rows'][1]['purchaseNo']); $this->assertSame(SupplierStatementMatcher::TYPE_MATCH, $result['rows'][1]['type']); } public function test_regression_keeps_real_missing_and_extra_lines_without_hiding_source_rows(): void { $mine = [ [ 'purchaseNo' => '911299', 'date' => '2026-10-10', 'skuCode' => 'XG007322', 'qty' => '30', 'amount' => '4530', ], [ 'purchaseNo' => '941858', 'date' => '2026-10-10', 'skuCode' => 'XG007322', 'qty' => '30', 'amount' => '4530', ], [ 'purchaseNo' => '923923', 'date' => '2026-10-03', 'skuCode' => 'CBG011611', 'qty' => '36', 'amount' => '2466', ], [ 'purchaseNo' => '923923', 'date' => '2026-10-04', 'skuCode' => 'CBG011611', 'qty' => '44', 'amount' => '3014', ], [ 'purchaseNo' => '937779', 'date' => '2026-10-20', 'skuCode' => 'SJ009363', 'qty' => '130', 'amount' => '12350', ], ]; $theirs = [ [ 'purchaseNo' => '941858', 'createdAt' => '2026.1', 'barcode' => 'XG007322N160V01', 'qty' => '30', 'amount' => '4530', ], [ 'purchaseNo' => '923923', 'createdAt' => '2026.1', 'barcode' => 'CBG011611N160V01', 'qty' => '80', 'amount' => '5480', ], [ 'purchaseNo' => '937779', 'createdAt' => '2026.1', 'barcode' => 'SJ009363N160V01', 'qty' => '130', 'amount' => '12350', ], [ 'purchaseNo' => '922003', 'createdAt' => '2026.1', 'barcode' => 'SJ009363N160V01', 'qty' => '20', 'amount' => '0', ], ]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $differences = array_values(array_filter( $result['rows'], static fn (array $row): bool => $row['type'] !== SupplierStatementMatcher::TYPE_MATCH, )); $this->assertSame(3, $result['summary']['match']); $this->assertSame(1, $result['summary']['supplierExtra']); $this->assertSame(1, $result['summary']['supplierMissing']); $this->assertSame(26890.0, $result['summary']['mineAmount']); $this->assertSame(22360.0, $result['summary']['theirsAmount']); $this->assertSame(4530.0, $result['summary']['differenceAmount']); $this->assertCount(2, $differences); $byPurchaseNo = collect($differences)->keyBy('purchaseNo'); $this->assertSame(SupplierStatementMatcher::TYPE_SUPPLIER_MISSING, $byPurchaseNo['911299']['type']); $this->assertSame(30.0, $byPurchaseNo['911299']['qtyDifference'] * -1); $this->assertSame('SJ009363N160V01', $byPurchaseNo['922003']['barcode']); $this->assertSame(20.0, $byPurchaseNo['922003']['qtyDifference']); } public function test_crossed_purchase_numbers_match_by_barcode_and_quantity(): void { $mine = [ [ 'purchaseNo' => '958052', 'date' => '2026-10-26', 'skuCode' => 'DSG004815', 'productCode' => 'DSG004815N1019V01', 'qty' => '30', 'unitPrice' => '69', 'amount' => '2070', ], [ 'purchaseNo' => '972032', 'date' => '2026-10-26', 'skuCode' => 'DSG004816', 'productCode' => 'DSG004816N1019V01', 'qty' => '30', 'unitPrice' => '69', 'amount' => '2070', ], ]; $theirs = [ [ 'purchaseNo' => '958052', 'createdAt' => '2026-10-26', 'barcode' => 'DSG004816N1019V01', 'qty' => '30', 'unitPrice' => '69', 'amount' => '2070', ], [ 'purchaseNo' => '972032', 'createdAt' => '2026-10-26', 'barcode' => 'DSG004815N1019V01', 'qty' => '30', 'unitPrice' => '69', 'amount' => '2070', ], ]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $rows = collect($result['rows'])->keyBy('minePurchaseNo'); $this->assertSame(2, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(0, $result['summary']['mismatch']); $this->assertSame(3, $rows['958052']['matchedByLayer']); $this->assertSame('972032', $rows['958052']['theirsPurchaseNo']); $this->assertTrue($rows['958052']['purchaseNoDifference']); $this->assertSame(3, $rows['972032']['matchedByLayer']); $this->assertSame('958052', $rows['972032']['theirsPurchaseNo']); $this->assertStringContainsString('采购单号不一致', $rows['958052']['note']); $this->assertStringContainsString('商品编码+数量匹配', $rows['958052']['note']); } public function test_barcode_and_quantity_match_before_express_fallback(): void { $mine = [[ 'expressNo' => '@sf123456', 'purchaseNo' => 'MINE-PO', 'date' => '2026-10-01', 'skuCode' => 'CZ000927', 'productCode' => 'CZ000927N1201V02', 'qty' => '7', 'amount' => '602', ]]; $theirs = [[ 'expressNo' => 'SF123456', 'purchaseNo' => '', 'createdAt' => '2026-10-09', 'barcode' => 'CZ000927N1201V01', 'qty' => '7', 'amount' => '602', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(1, $result['summary']['match']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(0, $result['summary']['mismatch']); $this->assertSame('SF123456', $result['rows'][0]['expressNo']); $this->assertSame(3, $result['rows'][0]['matchedByLayer']); $this->assertStringContainsString('商品编码+数量匹配', $result['rows'][0]['note']); } public function test_express_match_is_used_after_earlier_layers_and_aggregates_rows(): void { $mine = [ [ 'expressNo' => 'SF10001', 'purchaseNo' => '', 'date' => '2026-10-01', 'skuCode' => 'CZ000927', 'qty' => '2', 'amount' => '172', ], [ 'expressNo' => 'SF10001', 'purchaseNo' => '', 'date' => '2026-10-02', 'skuCode' => 'CZ000927', 'qty' => '5', 'amount' => '430', ], ]; $theirs = [[ 'expressNo' => 'SF10001', 'purchaseNo' => '', 'createdAt' => '2026-10-08', 'barcode' => 'CZ000927N1201V01', 'qty' => '8', 'amount' => '688', ]]; $result = (new SupplierStatementMatcher())->compare($mine, $theirs); $this->assertSame(0, $result['summary']['match']); $this->assertSame(1, $result['summary']['mismatch']); $this->assertSame(0, $result['summary']['supplierExtra']); $this->assertSame(0, $result['summary']['supplierMissing']); $this->assertSame(7.0, $result['rows'][0]['mineQty']); $this->assertSame(602.0, $result['rows'][0]['mineAmount']); $this->assertSame(1.0, $result['rows'][0]['qtyDifference']); $this->assertSame(4, $result['rows'][0]['matchedByLayer']); $this->assertStringContainsString('快递单号+商品编码匹配', $result['rows'][0]['note']); } }