修复商品成本残留组合父行与分单匹配 #596

Merged
chenxi merged 2 commits from fix/chenxi-product-cost-bundle-residual-parents into main 2026-10-26 14:57:45 +08:00
2 changed files with 531 additions and 27 deletions

View File

@ -108,6 +108,9 @@ class SyncStoreReceiptProductCost extends Command
/** 线上代发价格的钉钉代发来源。 */
private const DING_DAIFA_TABLE = 'ding_online_daifa_price_mirror';
/** @var array<string,list<array{shop_sku_id:string,components:array<string,array{qty:float,amount:float}>,amount:float}>>|null */
private ?array $goodsProfitProfileScopeIndex = null;
/**
* 订单侧两个来源:淘系(企微客户订单)与聚水潭订单。
* 两边的仓库字段不同:淘系明细没有 send_warehouse,用订单上的 wms_co_id;
@ -2089,13 +2092,28 @@ class SyncStoreReceiptProductCost extends Command
return;
}
$nonLeafOrderIds = [];
foreach ($unique as $row) {
$parentOrderId = trim((string) ($row['parent_o_id'] ?? ''));
if ($parentOrderId !== '' && $parentOrderId !== '0') {
$nonLeafOrderIds[$parentOrderId] = true;
}
}
// 分单订单可能把同一组合父行拆到多个末级子单,商品利润组件仍是整单口径。
// 只有所有子单的组件完全一致且父行金额合计对平时,才合并成一组组件。
$this->applyGoodsProfitSplitFamilies($unique, $stats, $profiles);
$groups = [];
foreach ($unique as $key => $row) {
$source = trim((string) ($row['source'] ?? ''));
$orderId = trim((string) ($row['o_id'] ?? ''));
$shopSkuId = trim((string) ($row['shop_sku_id'] ?? ''));
$shopItemId = trim((string) ($row['shop_i_id'] ?? ''));
if (in_array('', [$source, $orderId, $shopSkuId, $shopItemId], true)) {
if (
in_array('', [$source, $orderId, $shopSkuId, $shopItemId], true)
|| isset($nonLeafOrderIds[$orderId])
) {
continue;
}
$profileKey = self::goodsProfitSplitProfileKey($source, $orderId, $shopSkuId, $shopItemId);
@ -2137,22 +2155,30 @@ class SyncStoreReceiptProductCost extends Command
}
$componentCodes = array_fill_keys(array_keys($components), true);
$parentRows = [];
$bundleParentRows = [];
$activeBundleParentRows = [];
foreach ($groupKeys as $key) {
if (! isset($unique[$key])) {
continue;
}
$sku = mb_strtoupper(trim((string) ($unique[$key]['sku'] ?? '')));
if ($sku === '' || isset($componentCodes[$sku])) {
continue;
}
$parentRows[$sku][] = $key;
if (self::isBundleParentRow($unique[$key])) {
$bundleParentRows[$sku][] = $key;
$quantity = (float) ($unique[$key]['qty'] ?? 0);
$amount = (float) ($unique[$key]['amount'] ?? 0);
if (
self::isBundleParentRow($unique[$key])
&& ($quantity > 0 || abs($amount) > 0.005)
) {
$activeBundleParentRows[$sku][] = $key;
}
}
// 订单镜像可能同时残留上一版组合组件。存在唯一组合父行时,只按
// 该父行与商品利润原表组件对账;对平后同组旧组件会一并剔除。
if (count($bundleParentRows) === 1) {
$parentRows = $bundleParentRows;
// 订单镜像可能同时残留上一版零数量父行。存在唯一有效组合父行时,
// 只按该父行与商品利润原表组件对账;零数量旧父行随同组一起剔除。
if (count($activeBundleParentRows) === 1) {
$parentRows = $activeBundleParentRows;
}
if (count($parentRows) !== 1) {
@ -2199,24 +2225,13 @@ class SyncStoreReceiptProductCost extends Command
foreach ($groupKeys as $key) {
unset($unique[$key]);
}
foreach ($validComponents as $code => $component) {
$rowKey = (string) ($firstParent['row_key'] ?? $parentSku)
. ':goods-profit:' . sha1($parentSku . "\0" . $code);
$unique[$rowKey] = array_merge($firstParent, [
'sku' => $code,
'sku_type' => 'normal',
'base_price' => 0.0,
'item_name' => $code,
'qty' => round((float) $component['qty'], 4),
'amount' => round((float) $component['amount'], 2),
'warehouse' => $resolvedWarehouse,
'parent_o_id' => '',
'row_key' => $rowKey,
'src_combine_sku_id' => '',
'properties_value' => '',
'bundle_component_source' => 'goods_profit_current_order',
]);
}
$this->addGoodsProfitSplitComponents(
$unique,
$firstParent,
$parentSku,
$validComponents,
$resolvedWarehouse,
);
$stats['bundle_stats']['goods_profit_split_groups']++;
$stats['bundle_stats']['goods_profit_split_parent_rows'] += count($parentKeys);
@ -2225,6 +2240,373 @@ class SyncStoreReceiptProductCost extends Command
$stats['bundle_stats']['goods_profit_split_component_codes'][$code] = true;
}
}
// 极少数订单的活动父行使用了新平台 SKU,而商品利润组件仍挂在旧平台 SKU 下。
// 同一订单、同一平台款式下仅有一个组件集合金额与活动父行对平时,允许跨 SKU 替换。
$this->applyGoodsProfitCrossSkuSplits($unique, $stats, $profiles);
}
/**
* 合并分单末级父行后应用商品利润组件。
*
* @param array<string,array<string,mixed>> $unique
* @param array<string,mixed> $stats
* @param array<string,array<string,mixed>> $profiles
*/
private function applyGoodsProfitSplitFamilies(array &$unique, array &$stats, array $profiles): void
{
$families = [];
foreach ($unique as $key => $row) {
$parentOrderId = trim((string) ($row['parent_o_id'] ?? ''));
if (
$parentOrderId === ''
|| $parentOrderId === '0'
|| ! self::isBundleParentRow($row)
) {
continue;
}
$quantity = (float) ($row['qty'] ?? 0);
$amount = (float) ($row['amount'] ?? 0);
if ($quantity <= 0 && abs($amount) <= 0.005) {
continue;
}
$source = trim((string) ($row['source'] ?? ''));
$shopSkuId = trim((string) ($row['shop_sku_id'] ?? ''));
$shopItemId = trim((string) ($row['shop_i_id'] ?? ''));
$parentSku = mb_strtoupper(trim((string) ($row['sku'] ?? '')));
$orderId = trim((string) ($row['o_id'] ?? ''));
if (
in_array('', [$source, $orderId, $shopSkuId, $shopItemId, $parentSku], true)
) {
continue;
}
$familyKey = $source . "\0" . $parentOrderId . "\0" . $shopSkuId . "\0" . $shopItemId;
$families[$familyKey]['first_parent_key'] = $families[$familyKey]['first_parent_key'] ?? $key;
$families[$familyKey]['parent_keys'][] = $key;
$families[$familyKey]['parent_skus'][$parentSku] = true;
$families[$familyKey]['profile_keys'][
self::goodsProfitSplitProfileKey($source, $orderId, $shopSkuId, $shopItemId)
] = true;
}
foreach ($families as $family) {
if (count($family['parent_keys']) < 2 || count($family['parent_skus']) !== 1) {
continue;
}
$componentSets = [];
foreach (array_keys($family['profile_keys']) as $profileKey) {
$components = $profiles[$profileKey]['components'] ?? null;
if (! is_array($components) || $components === []) {
continue 2;
}
$componentSets[] = $components;
}
if ($componentSets === []) {
continue;
}
$components = reset($componentSets);
foreach ($componentSets as $componentSet) {
if ($componentSet !== $components) {
continue 2;
}
}
$validComponents = self::validGoodsProfitComponents($components);
$componentAmount = (float) array_sum(array_column($validComponents, 'amount'));
$parentAmount = 0.0;
foreach ($family['parent_keys'] as $parentKey) {
if (isset($unique[$parentKey])) {
$parentAmount += (float) ($unique[$parentKey]['amount'] ?? 0);
}
}
if (
$validComponents === []
|| $parentAmount <= 0
|| abs(round($componentAmount - $parentAmount, 2)) > 0.01
) {
continue;
}
$firstParent = $unique[$family['first_parent_key']];
$familyRootOrderId = trim((string) ($firstParent['parent_o_id'] ?? ''));
$resolvedWarehouse = '';
foreach ($unique as $row) {
$rowRootOrderId = trim((string) ($row['parent_o_id'] ?? ''));
if ($rowRootOrderId === '' || $rowRootOrderId === '0') {
$rowRootOrderId = trim((string) ($row['o_id'] ?? ''));
}
if (
trim((string) ($row['source'] ?? '')) !== trim((string) ($firstParent['source'] ?? ''))
|| $rowRootOrderId !== $familyRootOrderId
|| trim((string) ($row['shop_sku_id'] ?? '')) !== trim((string) ($firstParent['shop_sku_id'] ?? ''))
|| trim((string) ($row['shop_i_id'] ?? '')) !== trim((string) ($firstParent['shop_i_id'] ?? ''))
) {
continue;
}
if (trim((string) ($row['warehouse'] ?? '')) !== '') {
$resolvedWarehouse = trim((string) $row['warehouse']);
break;
}
}
$componentCodes = array_fill_keys(array_keys($validComponents), true);
$parentRows = 0;
foreach ($unique as $key => $row) {
$rowRootOrderId = trim((string) ($row['parent_o_id'] ?? ''));
if ($rowRootOrderId === '' || $rowRootOrderId === '0') {
$rowRootOrderId = trim((string) ($row['o_id'] ?? ''));
}
if (
trim((string) ($row['source'] ?? '')) !== trim((string) ($firstParent['source'] ?? ''))
|| $rowRootOrderId !== $familyRootOrderId
|| trim((string) ($row['shop_sku_id'] ?? '')) !== trim((string) ($firstParent['shop_sku_id'] ?? ''))
|| trim((string) ($row['shop_i_id'] ?? '')) !== trim((string) ($firstParent['shop_i_id'] ?? ''))
) {
continue;
}
$sku = mb_strtoupper(trim((string) ($row['sku'] ?? '')));
if (! self::isBundleParentRow($row) && ! isset($componentCodes[$sku])) {
continue;
}
if (self::isBundleParentRow($row)) {
$parentRows++;
}
unset($unique[$key]);
}
$parentSku = (string) array_key_first($family['parent_skus']);
$this->addGoodsProfitSplitComponents(
$unique,
$firstParent,
$parentSku,
$validComponents,
$resolvedWarehouse,
);
$stats['bundle_stats']['goods_profit_split_groups']++;
$stats['bundle_stats']['goods_profit_split_parent_rows'] += $parentRows;
$stats['bundle_stats']['goods_profit_split_component_rows'] += count($validComponents);
foreach (array_keys($validComponents) as $code) {
$stats['bundle_stats']['goods_profit_split_component_codes'][$code] = true;
}
}
}
/**
* 活动父行平台 SKU 与商品利润组件平台 SKU 不一致时,只在组件集合唯一时替换。
*
* @param array<string,array<string,mixed>> $unique
* @param array<string,mixed> $stats
* @param array<string,array<string,mixed>> $profiles
*/
private function applyGoodsProfitCrossSkuSplits(array &$unique, array &$stats, array $profiles): void
{
$profilesByScope = $this->buildGoodsProfitProfileScopeIndex($profiles);
$parentsByScope = [];
foreach ($unique as $key => $row) {
$source = trim((string) ($row['source'] ?? ''));
$orderId = trim((string) ($row['o_id'] ?? ''));
$shopSkuId = trim((string) ($row['shop_sku_id'] ?? ''));
$shopItemId = trim((string) ($row['shop_i_id'] ?? ''));
$sku = mb_strtoupper(trim((string) ($row['sku'] ?? '')));
if (
in_array('', [$source, $orderId, $shopSkuId, $shopItemId, $sku], true)
|| ! self::isBundleParentRow($row)
|| (float) ($row['qty'] ?? 0) <= 0
|| (float) ($row['amount'] ?? 0) <= 0
|| isset($profiles[self::goodsProfitSplitProfileKey($source, $orderId, $shopSkuId, $shopItemId)])
) {
continue;
}
$scopeKey = $source . "\0" . $orderId . "\0" . $shopItemId;
$parentsByScope[$scopeKey]['keys'][] = $key;
$parentsByScope[$scopeKey]['skus'][$sku] = true;
$parentsByScope[$scopeKey]['amount'] = ($parentsByScope[$scopeKey]['amount'] ?? 0.0)
+ (float) ($row['amount'] ?? 0);
$parentsByScope[$scopeKey]['shop_sku_id'] = $shopSkuId;
}
foreach ($parentsByScope as $scopeKey => $parentGroup) {
if (count($parentGroup['skus']) !== 1 || (float) ($parentGroup['amount'] ?? 0) <= 0) {
continue;
}
$candidates = [];
foreach ($profilesByScope[$scopeKey] ?? [] as $candidate) {
if (
$candidate['shop_sku_id'] !== $parentGroup['shop_sku_id']
&& abs(round((float) $candidate['amount'] - (float) $parentGroup['amount'], 2)) <= 0.01
) {
$candidates[] = $candidate;
}
}
if (count($candidates) !== 1) {
continue;
}
$candidate = $candidates[0];
$componentCodes = array_fill_keys(array_keys($candidate['components']), true);
[$source, $orderId, $shopItemId] = explode("\0", $scopeKey);
$linkedRows = false;
$resolvedWarehouse = '';
foreach ($unique as $row) {
if (
trim((string) ($row['source'] ?? '')) !== $source
|| trim((string) ($row['o_id'] ?? '')) !== $orderId
|| trim((string) ($row['shop_sku_id'] ?? '')) !== $candidate['shop_sku_id']
|| trim((string) ($row['shop_i_id'] ?? '')) !== $shopItemId
) {
continue;
}
if (trim((string) ($row['warehouse'] ?? '')) !== '') {
$resolvedWarehouse = trim((string) $row['warehouse']);
}
$linkedParent = mb_strtoupper(trim((string) ($row['src_combine_sku_id'] ?? '')));
if (
self::isBundleParentRow($row)
|| self::isChineseBundleParentSku($linkedParent)
) {
$linkedRows = true;
}
}
if (! $linkedRows) {
continue;
}
$firstParent = $unique[$parentGroup['keys'][0]];
foreach ($unique as $key => $row) {
if (
trim((string) ($row['source'] ?? '')) !== $source
|| trim((string) ($row['o_id'] ?? '')) !== $orderId
|| trim((string) ($row['shop_sku_id'] ?? '')) !== $candidate['shop_sku_id']
|| trim((string) ($row['shop_i_id'] ?? '')) !== $shopItemId
) {
continue;
}
$sku = mb_strtoupper(trim((string) ($row['sku'] ?? '')));
$linkedParent = mb_strtoupper(trim((string) ($row['src_combine_sku_id'] ?? '')));
if (
self::isBundleParentRow($row)
|| isset($componentCodes[$sku])
|| self::isChineseBundleParentSku($linkedParent)
) {
unset($unique[$key]);
}
}
foreach ($parentGroup['keys'] as $parentKey) {
unset($unique[$parentKey]);
}
$parentSku = (string) array_key_first($parentGroup['skus']);
$this->addGoodsProfitSplitComponents(
$unique,
$firstParent,
$parentSku,
$candidate['components'],
$resolvedWarehouse,
);
$stats['bundle_stats']['goods_profit_split_groups']++;
$stats['bundle_stats']['goods_profit_split_parent_rows'] += count($parentGroup['keys']);
$stats['bundle_stats']['goods_profit_split_component_rows'] += count($candidate['components']);
foreach (array_keys($candidate['components']) as $code) {
$stats['bundle_stats']['goods_profit_split_component_codes'][$code] = true;
}
}
}
/**
* 商品利润画像全场一致,跨 SKU 候选索引只构建一次,避免每个账单重复扫描。
*
* @param array<string,array<string,mixed>> $profiles
* @return array<string,list<array{shop_sku_id:string,components:array<string,array{qty:float,amount:float}>,amount:float}>>
*/
private function buildGoodsProfitProfileScopeIndex(array $profiles): array
{
if ($this->goodsProfitProfileScopeIndex !== null) {
return $this->goodsProfitProfileScopeIndex;
}
$index = [];
foreach ($profiles as $profileKey => $profile) {
$parts = explode("\0", $profileKey);
if (count($parts) !== 4 || ! is_array($profile['components'] ?? null)) {
continue;
}
$validComponents = self::validGoodsProfitComponents($profile['components']);
if ($validComponents === []) {
continue;
}
$scopeKey = $parts[0] . "\0" . $parts[1] . "\0" . $parts[3];
$index[$scopeKey][] = [
'shop_sku_id' => $parts[2],
'components' => $validComponents,
'amount' => round((float) array_sum(array_column($validComponents, 'amount')), 2),
];
}
return $this->goodsProfitProfileScopeIndex = $index;
}
/**
* @param array<string,mixed> $components
* @return array<string,array{qty:float,amount:float}>
*/
private static function validGoodsProfitComponents(array $components): array
{
$valid = [];
foreach ($components as $componentCode => $component) {
$code = mb_strtoupper(trim((string) $componentCode));
$quantity = (float) ($component['qty'] ?? 0);
$amount = (float) ($component['amount'] ?? 0);
if ($code === '' || $quantity <= 0 || $amount <= 0) {
continue;
}
$valid[$code] = [
'qty' => round($quantity, 4),
'amount' => round($amount, 2),
];
}
ksort($valid, SORT_STRING);
return $valid;
}
/**
* @param array<string,array<string,mixed>> $unique
* @param array<string,mixed> $firstParent
* @param array<string,array{qty:float,amount:float}> $components
*/
private function addGoodsProfitSplitComponents(
array &$unique,
array $firstParent,
string $parentSku,
array $components,
string $resolvedWarehouse,
): void {
foreach ($components as $code => $component) {
$rowKey = (string) ($firstParent['row_key'] ?? $parentSku)
. ':goods-profit:' . sha1($parentSku . "\0" . $code);
$unique[$rowKey] = array_merge($firstParent, [
'sku' => $code,
'sku_type' => 'normal',
'base_price' => 0.0,
'item_name' => $code,
'qty' => round((float) $component['qty'], 4),
'amount' => round((float) $component['amount'], 2),
'warehouse' => $resolvedWarehouse,
'parent_o_id' => '',
'row_key' => $rowKey,
'src_combine_sku_id' => '',
'properties_value' => '',
'bundle_component_source' => 'goods_profit_current_order',
'keep_after_leaf_filter' => true,
]);
}
}
/** @param array<string,mixed> $row */
@ -2774,7 +3156,8 @@ class SyncStoreReceiptProductCost extends Command
if ($parentIds !== []) {
$leaves = array_filter(
$unique,
static fn (array $row): bool => ! isset($parentIds[trim((string) $row['o_id'])])
static fn (array $row): bool => ! empty($row['keep_after_leaf_filter'])
|| ! isset($parentIds[trim((string) $row['o_id'])])
);
// 兜底:理论上不会出现全部被过滤(除非数据自相矛盾),真有则保留原集合避免整单丢失。
if ($leaves !== []) {

View File

@ -641,6 +641,127 @@ class StoreReceiptProductCostLineTest extends TestCase
$this->assertSame(649.0, round(array_sum(array_column($lines, 'amount')), 2));
}
public function test_goods_profit_bundle_parent_ignores_zero_quantity_stale_parent_rows(): void
{
$profileKey = "jst\0" . "8986587\0" . "3533103677164802\0" . 'GOODS-ITEM-ZERO-PARENTS';
$components = [
'SJ038020N1410V02' => ['qty' => 1, 'amount' => 377.31],
'SJ038069N1410V01' => ['qty' => 3, 'amount' => 42.69],
];
[$lines, , , , , $stats] = $this->costLines([
$this->row('old-parent-1', '运营组合套装12630', 'GOODS-ITEM-ZERO-PARENTS', 0, 0),
$this->row('old-parent-2', '运营组合套装13014', 'GOODS-ITEM-ZERO-PARENTS', 0, 0),
$this->row('current-parent', '运营组合套装13015', 'GOODS-ITEM-ZERO-PARENTS', 1, 420),
], [], [
$profileKey => [
'source' => 'goods_profit_current_order',
'components' => $components,
],
]);
$codes = array_column($lines, 'sku');
sort($codes, SORT_STRING);
$this->assertSame(['SJ038020N1410V02', 'SJ038069N1410V01'], $codes);
$this->assertSame(420.0, round(array_sum(array_column($lines, 'amount')), 2));
$this->assertSame(1, $stats['bundle_stats']['goods_profit_split_groups']);
$this->assertSame(1, $stats['bundle_stats']['goods_profit_split_parent_rows']);
}
public function test_goods_profit_bundle_combines_split_parent_branches_before_reconcile(): void
{
$components = [
'DNZ038526N1280V01' => ['qty' => 2, 'amount' => 1111.84],
'GBGJ-DNZ038401N1440V01' => ['qty' => 2, 'amount' => 954.58],
'GBGJ-ZWJ024903AN1407V01' => ['qty' => 2, 'amount' => 633.90],
'GBGJ-ZWJ024903BN1407V01' => ['qty' => 2, 'amount' => 633.90],
];
$common = [
'shop_sku_id' => 'SPLIT-SKU',
];
[$lines, , , , , $stats] = $this->costLines([
$this->row('split-root', 'MKH-运营组合套装260706016', 'SPLIT-ITEM', 2, 3334.22, '', $common + [
'o_id' => 'SPLIT-ROOT',
]),
$this->row('split-child-1', 'MKH-运营组合套装260706016', 'SPLIT-ITEM', 1, 1667.11, '', $common + [
'o_id' => 'SPLIT-CHILD-1',
'parent_o_id' => 'SPLIT-ROOT',
]),
$this->row('split-child-2', 'MKH-运营组合套装260706016', 'SPLIT-ITEM', 1, 1667.11, '', $common + [
'o_id' => 'SPLIT-CHILD-2',
'parent_o_id' => 'SPLIT-ROOT',
]),
], [], [
"jst\0SPLIT-CHILD-1\0SPLIT-SKU\0SPLIT-ITEM" => [
'source' => 'goods_profit_current_order',
'components' => $components,
],
"jst\0SPLIT-CHILD-2\0SPLIT-SKU\0SPLIT-ITEM" => [
'source' => 'goods_profit_current_order',
'components' => $components,
],
]);
$bySku = collect($lines)->keyBy('sku');
$this->assertCount(4, $lines);
$this->assertSame(2.0, $bySku['DNZ038526N1280V01']['qty']);
$this->assertSame(3334.22, round(array_sum(array_column($lines, 'amount')), 2));
$this->assertSame(1, $stats['bundle_stats']['goods_profit_split_groups']);
$this->assertSame(3, $stats['bundle_stats']['goods_profit_split_parent_rows']);
}
public function test_goods_profit_bundle_uses_unique_component_group_from_stale_shop_sku(): void
{
$components = [
'SJ038020N1410V02' => ['qty' => 1, 'amount' => 377.31],
'SJ038069N1410V01' => ['qty' => 3, 'amount' => 42.69],
];
$common = [
'o_id' => 'CROSS-SKU-ORDER',
'shop_i_id' => 'CROSS-SKU-ITEM',
];
[$lines, , , , , $stats] = $this->costLines([
$this->row('old-parent', '运营组合套装12630', 'CROSS-SKU-ITEM', 0, 0, '', $common + [
'shop_sku_id' => 'OLD-SKU',
]),
$this->row('component-1', 'SJ038020N1410V02', 'CROSS-SKU-ITEM', 1, 377.31, '运营组合套装12630', $common + [
'shop_sku_id' => 'OLD-SKU',
]),
$this->row('component-2', 'SJ038069N1410V01', 'CROSS-SKU-ITEM', 3, 42.69, '运营组合套装12630', $common + [
'shop_sku_id' => 'OLD-SKU',
]),
$this->row('current-parent', '运营组合套装13011', 'CROSS-SKU-ITEM', 1, 420, '', $common + [
'shop_sku_id' => 'NEW-SKU',
]),
$this->row('separate-item', 'SN021702N230V01', 'CROSS-SKU-ITEM', 1, 49.97, '', [
'o_id' => 'CROSS-SKU-CHILD',
'parent_o_id' => 'CROSS-SKU-ORDER',
'shop_i_id' => 'CROSS-SKU-ITEM',
'shop_sku_id' => 'OTHER-SKU',
]),
], [], [
"jst\0CROSS-SKU-ORDER\0OLD-SKU\0CROSS-SKU-ITEM" => [
'source' => 'goods_profit_current_order',
'components' => $components,
],
]);
$codes = array_column($lines, 'sku');
sort($codes, SORT_STRING);
$this->assertSame([
'SJ038020N1410V02',
'SJ038069N1410V01',
'SN021702N230V01',
], $codes);
$this->assertSame(469.97, round(array_sum(array_column($lines, 'amount')), 2));
$this->assertSame(1, $stats['bundle_stats']['goods_profit_split_groups']);
$this->assertSame(1, $stats['bundle_stats']['goods_profit_split_parent_rows']);
}
public function test_goods_profit_current_order_split_is_not_applied_when_amount_differs(): void
{
$profileKey = "jst\0" . "8986587\0" . "3533103677164802\0" . 'GOODS-ITEM-3';