修复业绩明细日期前缀串单并增加防碰撞匹配
This commit is contained in:
parent
ad8da1264d
commit
acad1f4577
@ -23,9 +23,9 @@ use Throwable;
|
||||
*
|
||||
* 口径(已与业务确认):
|
||||
* - 账单侧:按「总账备注」筛选(与店铺收款账户账单页同一口径),
|
||||
* 再按「店铺 + 清洗后订单号」汇总收入金额与收款笔数
|
||||
* 再按「店铺 + 完整线上单号」汇总收入金额与收款笔数
|
||||
* - 订单侧:销售全量按「店铺 + 线上订单号」先聚合求和(一单多品/多子订单在此合并),金额取 goods_count > 0 的有效行
|
||||
* - 单号清洗:去除日期前缀(如 260630-)、空格、横杠后按核心号匹配
|
||||
* - 单号匹配:优先完整线上单号精确匹配;仅当核心号唯一时允许兼容回退,多个完整单号共用核心号时禁止合并
|
||||
* - 店铺:账单用「线上真实店铺」优先,订单侧用销售全量店铺名,统一把下划线归一为横杠
|
||||
* - 同一店铺同一订单号多笔收款 → 标注「同单多笔收款」,商品金额只挂一次
|
||||
*/
|
||||
@ -448,15 +448,17 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string,float> $orderAmount 订单侧按「店铺 + 核心单号」聚合的商品金额(商品金额求和) */
|
||||
/** @var array<string,float> $orderAmount 订单侧按「店铺 + 完整线上单号」聚合的商品金额(商品金额求和) */
|
||||
$orderAmount = [];
|
||||
/** @var array<string,true> $orderSeen 订单侧出现过的「店铺 + 核心单号」(含 0 元行) */
|
||||
/** @var array<string,true> $orderSeen 订单侧出现过的「店铺 + 完整线上单号」(含 0 元行) */
|
||||
$orderSeen = [];
|
||||
/** @var array<string,array<string,true>> $coresByCore 核心单号 → 出现过的店铺集合 */
|
||||
$coresByCore = [];
|
||||
/** @var array<string,array<string,array<string,true>>> $identitiesByCore 核心单号 → 店铺 → 完整线上单号 */
|
||||
$identitiesByCore = [];
|
||||
/** @var array<string,true> $coreSeen 订单侧出现过的核心单号(含店铺名为空的行) */
|
||||
$coreSeen = [];
|
||||
/** @var array<string,float> $orderVirtualPaid 虚拟条码(X0000x)明细行的实付金额合计(店铺 + 核心单号) */
|
||||
/** @var array<string,float> $orderVirtualPaid 虚拟条码(X0000x)明细行的实付金额合计(店铺 + 完整线上单号) */
|
||||
$orderVirtualPaid = [];
|
||||
// 订单侧改用「订单原始数据」(企微客户订单 + 聚水潭订单),按线上订单号取商品金额合计。
|
||||
// 两个来源数据量大(企微 500 万单 / 690 万行),先用临时表收敛候选单号,
|
||||
@ -575,19 +577,21 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
}
|
||||
$this->info('订单侧命中(店铺 × 订单)行数:' . count($orderRows) . ',实付行数:' . count($paidRows));
|
||||
|
||||
/** @var array<string,float> $orderPaid 订单侧实付金额(店铺 + 核心单号) */
|
||||
/** @var array<string,float> $orderPaid 订单侧实付金额(店铺 + 完整线上单号) */
|
||||
$orderPaid = [];
|
||||
/** @var array<string,float> $orderPaidAlt 订单侧「卖家实收」金额(双口径兜底用) */
|
||||
$orderPaidAlt = [];
|
||||
/** @var array<string,float> $orderPaidPay 订单侧「应付」金额(实收缺失时的退化口径) */
|
||||
$orderPaidPay = [];
|
||||
foreach ($paidRows as $row) {
|
||||
$core = self::cleanOrderNo((string) $row['so_id']);
|
||||
$rawNo = (string) $row['so_id'];
|
||||
$core = self::cleanOrderNo($rawNo);
|
||||
$identity = self::orderIdentity($rawNo);
|
||||
if ($core === '') {
|
||||
continue;
|
||||
}
|
||||
$shopKey = self::normalizeShop((string) ($row['shop_name'] ?? ''));
|
||||
$key = $shopKey . '|' . $core;
|
||||
$key = $shopKey . '|' . $identity;
|
||||
$orderPaid[$key] = ($orderPaid[$key] ?? 0.0) + (float) $row['paid'];
|
||||
$orderPaidAlt[$key] = ($orderPaidAlt[$key] ?? 0.0) + (float) ($row['paid_alt'] ?? 0.0);
|
||||
$orderPaidPay[$key] = ($orderPaidPay[$key] ?? 0.0) + (float) ($row['paid_pay'] ?? 0.0);
|
||||
@ -596,6 +600,7 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
// 商品金额数组仍只由商品明细构建:无明细时商品金额保持为空、按口径标「订单无商品金额」。
|
||||
$orderSeen[$key] = true;
|
||||
$coreSeen[$core] = true;
|
||||
$identitiesByCore[$core][$shopKey][$identity] = true;
|
||||
if ($shopKey !== '') {
|
||||
$coresByCore[$core][$shopKey] = true;
|
||||
}
|
||||
@ -603,14 +608,17 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
|
||||
unset($paidRows, $row);
|
||||
foreach ($orderRows as $row) {
|
||||
$core = self::cleanOrderNo((string) $row['so_id']);
|
||||
$rawNo = (string) $row['so_id'];
|
||||
$core = self::cleanOrderNo($rawNo);
|
||||
$identity = self::orderIdentity($rawNo);
|
||||
if ($core === '') {
|
||||
continue;
|
||||
}
|
||||
$shopKey = self::normalizeShop((string) ($row['shop_name'] ?? ''));
|
||||
$key = $shopKey . '|' . $core;
|
||||
$key = $shopKey . '|' . $identity;
|
||||
$orderSeen[$key] = true;
|
||||
$coreSeen[$core] = true;
|
||||
$identitiesByCore[$core][$shopKey][$identity] = true;
|
||||
if ($shopKey !== '') {
|
||||
$coresByCore[$core][$shopKey] = true;
|
||||
}
|
||||
@ -665,21 +673,25 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
// 两个口径各跑一遍同一条取数链(同店同号 → 店铺名不一致 → 同号多条 → 空店铺兜底):
|
||||
// 商品金额用于「商品金额」列,订单实付用于「订单实付金额」列与比对基准。
|
||||
[$productStatus, $productAmount, $productRemark] = $this->matchOrderSide(
|
||||
(string) $group['online_order_no'],
|
||||
$core,
|
||||
(string) $group['shop_key'],
|
||||
$orderAmount,
|
||||
$orderSeen,
|
||||
$coreSeen,
|
||||
$coresByCore,
|
||||
$identitiesByCore,
|
||||
self::REMARK_NO_AMOUNT,
|
||||
);
|
||||
[$paidStatus, $paidAmount, $paidRemark] = $this->matchOrderSide(
|
||||
(string) $group['online_order_no'],
|
||||
$core,
|
||||
(string) $group['shop_key'],
|
||||
$orderPaidNet,
|
||||
$orderSeen,
|
||||
$coreSeen,
|
||||
$coresByCore,
|
||||
$identitiesByCore,
|
||||
self::REMARK_NO_PAID,
|
||||
);
|
||||
|
||||
@ -706,12 +718,14 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
// 「先备注规则、后口径兜底」的顺序判定),避免把有明确原因的行悄悄抹平。
|
||||
{
|
||||
[$altStatus, $altAmount] = $this->matchOrderSide(
|
||||
(string) $group['online_order_no'],
|
||||
$core,
|
||||
(string) $group['shop_key'],
|
||||
$orderPaidAlt,
|
||||
$orderSeen,
|
||||
$coreSeen,
|
||||
$coresByCore,
|
||||
$identitiesByCore,
|
||||
self::REMARK_NO_PAID,
|
||||
);
|
||||
$altMatched = $altStatus === self::STATUS_MATCHED && $altAmount !== null;
|
||||
@ -722,12 +736,14 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
// 或主单没写实收而子单写了值的情况都覆盖到)。
|
||||
if (! ($altMatched && abs($income + $promoAmount - $altValue) <= 0.02)) {
|
||||
[$payStatus, $payValue] = $this->matchOrderSide(
|
||||
(string) $group['online_order_no'],
|
||||
$core,
|
||||
(string) $group['shop_key'],
|
||||
$orderPaidPay,
|
||||
$orderSeen,
|
||||
$coreSeen,
|
||||
$coresByCore,
|
||||
$identitiesByCore,
|
||||
self::REMARK_NO_PAID,
|
||||
);
|
||||
if ($payStatus === self::STATUS_MATCHED && $payValue !== null) {
|
||||
@ -764,7 +780,12 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
$buffer[] = [
|
||||
'report_month' => $reportMonth,
|
||||
'platform' => $group['platform'],
|
||||
'row_key' => md5($reportMonth . '|' . $group['platform'] . '|' . $group['shop_key'] . '|' . $core),
|
||||
'row_key' => self::groupRowKey(
|
||||
$reportMonth,
|
||||
(string) $group['platform'],
|
||||
(string) $group['shop_key'],
|
||||
(string) $group['order_identity']
|
||||
),
|
||||
'occurred_date' => $group['occurred_date'],
|
||||
'store_name' => $group['store_name'],
|
||||
'online_order_no' => $group['online_order_no'] !== '' ? $group['online_order_no'] : null,
|
||||
@ -1021,7 +1042,12 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowKey = md5($reportMonth . '|' . $platform . '|' . $group['shop_key'] . '|' . $group['order_core_no']);
|
||||
$rowKey = self::groupRowKey(
|
||||
$reportMonth,
|
||||
$platform,
|
||||
(string) $group['shop_key'],
|
||||
(string) $group['order_identity']
|
||||
);
|
||||
if (! isset($existing[$rowKey])) {
|
||||
continue;
|
||||
}
|
||||
@ -1204,9 +1230,10 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
|
||||
$rawNo = trim((string) ($bill->{$orderColumn} ?? ''));
|
||||
$core = self::cleanOrderNo($rawNo);
|
||||
$identity = self::orderIdentity($rawNo);
|
||||
$realStore = $realStoreColumn !== '' ? (string) ($bill->{$realStoreColumn} ?? '') : '';
|
||||
$shopKey = self::normalizeShop($realStore !== '' ? $realStore : (string) $bill->store_name);
|
||||
$key = $shopKey . '|' . $core;
|
||||
$key = $shopKey . '|' . $identity;
|
||||
|
||||
if (! isset($groups[$key])) {
|
||||
$groups[$key] = [
|
||||
@ -1216,6 +1243,7 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
'shop_key' => $shopKey,
|
||||
'online_order_no' => $rawNo,
|
||||
'order_core_no' => $core,
|
||||
'order_identity' => $identity,
|
||||
'occurred_date' => substr((string) $bill->{$timeColumn}, 0, 10),
|
||||
'payment_count' => 0,
|
||||
'income_amount' => 0.0,
|
||||
@ -1235,13 +1263,14 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
// 同一遍读取同时累计补行,避免把整月原始账单留到第二遍。
|
||||
// 首笔元数据、无单号按账单项目拆分、收入行随后出现的语义与原有补行一致。
|
||||
if ($bucket !== 'income') {
|
||||
$promoKey = $core === '' ? $shopKey . '||' . $billItem : $key;
|
||||
$promoKey = $identity === '' ? $shopKey . '||' . $billItem : $key;
|
||||
if (! isset($promoOnlyGroups[$promoKey])) {
|
||||
$promoOnlyGroups[$promoKey] = [
|
||||
'shop_key' => $shopKey,
|
||||
'store_name' => $realStore !== '' ? $realStore : $storeName,
|
||||
'online_order_no' => $rawNo,
|
||||
'order_core_no' => $core,
|
||||
'order_identity' => $identity,
|
||||
'bill_item' => $billItem,
|
||||
'occurred_date' => substr((string) $bill->{$timeColumn}, 0, 10),
|
||||
'discount_amount' => 0.0,
|
||||
@ -1308,7 +1337,7 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
}
|
||||
// A 方案:挂不到订单行的优惠/冲减也补成行(收入/商品/差额留空),金额不丢。
|
||||
foreach ($promoOnlyGroups as $key => $group) {
|
||||
if ($group['order_core_no'] !== '' && isset($groups[$group['shop_key'] . '|' . $group['order_core_no']])) {
|
||||
if ($group['order_identity'] !== '' && isset($groups[$group['shop_key'] . '|' . $group['order_identity']])) {
|
||||
unset($promoOnlyGroups[$key]);
|
||||
}
|
||||
}
|
||||
@ -1327,9 +1356,10 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
*/
|
||||
private static function promoOnlyRow(string $reportMonth, string $platform, array $group, mixed $now): array
|
||||
{
|
||||
$identity = (string) ($group['order_identity'] ?? '');
|
||||
$core = (string) $group['order_core_no'];
|
||||
$rowKey = $core !== ''
|
||||
? md5($reportMonth . '|' . $platform . '|' . $group['shop_key'] . '|' . $core)
|
||||
$rowKey = $identity !== ''
|
||||
? self::groupRowKey($reportMonth, $platform, (string) $group['shop_key'], $identity)
|
||||
: md5($reportMonth . '|' . $platform . '|' . $group['shop_key'] . '|promo|' . $group['bill_item']);
|
||||
|
||||
return [
|
||||
@ -1403,6 +1433,31 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
return self::orderCoreNo($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整线上单号身份:保留日期前缀,只统一空白、横杠形态和大小写。
|
||||
*
|
||||
* 拼多多的「260627-...」「260713-...」是两个不同订单,不能按去掉
|
||||
* 日期前缀后的核心号合并;完整单号用于账单分组与订单侧精确匹配,
|
||||
* 核心号只在唯一候选时作为兼容回退。
|
||||
*/
|
||||
private static function orderIdentity(string $value): string
|
||||
{
|
||||
$value = str_replace("\u{3000}", ' ', trim($value));
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = preg_replace('/[\x{FF0D}\x{2014}\x{2013}\x{2010}]/u', '-', $value) ?? $value;
|
||||
$value = preg_replace('/\s+/u', '', $value) ?? '';
|
||||
|
||||
return strtoupper($value);
|
||||
}
|
||||
|
||||
private static function groupRowKey(string $reportMonth, string $platform, string $shopKey, string $identity): string
|
||||
{
|
||||
return md5($reportMonth . '|' . $platform . '|' . $shopKey . '|' . $identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 种菜订单号集合:种菜明细表的订单号 ∪ 聚水潭订单标签明细里「是否种菜订单=是」的订单号。
|
||||
*
|
||||
@ -1558,11 +1613,13 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* 按「店铺 + 订单号」在订单侧取金额,返回 [匹配状态, 订单侧金额, 原因标注]。
|
||||
* 按「店铺 + 完整线上单号」在订单侧取金额,返回 [匹配状态, 订单侧金额, 原因标注]。
|
||||
*
|
||||
* 两个比对基准共用这一条取数链:
|
||||
* 同店同号 → 店铺名不一致(订单号唯一)→ 同号多条(多店铺合计)→ 空店铺兜底 → 订单表无此单。
|
||||
* 同店完整单号 → 同店核心号唯一时回退 → 跨店唯一时回退 → 空店铺唯一时回退。
|
||||
* 核心号一旦对应多个完整线上单号就视为歧义,不再合并金额,避免不同订单串单。
|
||||
*
|
||||
* @param array<string,array<string,array<string,true>>> $identitiesByCore 核心单号 → 店铺 → 完整线上单号
|
||||
* @param array<string,float> $amounts 订单侧金额集合(商品金额或订单实付)
|
||||
* @param array<string,bool> $orderSeen 订单侧出现过的「店铺|单号」
|
||||
* @param array<string,bool> $coreSeen 订单侧出现过的单号
|
||||
@ -1570,12 +1627,14 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
* @return array{0:string,1:?float,2:string}
|
||||
*/
|
||||
private function matchOrderSide(
|
||||
string $onlineOrderNo,
|
||||
string $core,
|
||||
string $shopKey,
|
||||
array $amounts,
|
||||
array $orderSeen,
|
||||
array $coreSeen,
|
||||
array $coresByCore,
|
||||
array $identitiesByCore,
|
||||
string $noAmountRemark,
|
||||
): array {
|
||||
if ($core === '') {
|
||||
@ -1586,8 +1645,9 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
return [self::STATUS_UNMATCHED, null, self::REMARK_BAD_FORMAT];
|
||||
}
|
||||
|
||||
$exactKey = $shopKey . '|' . $core;
|
||||
if (isset($orderSeen[$exactKey])) {
|
||||
$identity = self::orderIdentity($onlineOrderNo);
|
||||
$exactKey = $shopKey . '|' . $identity;
|
||||
if ($identity !== '' && isset($orderSeen[$exactKey])) {
|
||||
if (isset($amounts[$exactKey])) {
|
||||
$remark = count($coresByCore[$core] ?? []) > 1 ? self::REMARK_DUP_ORDER : '';
|
||||
|
||||
@ -1598,41 +1658,43 @@ class SyncStoreReceiptOrderMatches extends Command
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
}
|
||||
|
||||
if (isset($coreSeen[$core]) && count($coresByCore[$core] ?? []) === 1) {
|
||||
// 单号一致但店铺名称对不上:仍然带上金额,标注「店铺不一致」便于复核。
|
||||
$otherKey = array_key_first($coresByCore[$core]) . '|' . $core;
|
||||
if (isset($amounts[$otherKey])) {
|
||||
return [self::STATUS_MATCHED, round((float) $amounts[$otherKey], 2), self::REMARK_SHOP_DIFF];
|
||||
if (isset($coreSeen[$core])) {
|
||||
$sameShopIdentities = array_keys($identitiesByCore[$core][$shopKey] ?? []);
|
||||
if (count($sameShopIdentities) > 1) {
|
||||
return [self::STATUS_UNMATCHED, null, self::REMARK_DUP_ORDER];
|
||||
}
|
||||
if (count($sameShopIdentities) === 1) {
|
||||
$fallbackKey = $shopKey . '|' . $sameShopIdentities[0];
|
||||
if (isset($amounts[$fallbackKey])) {
|
||||
$remark = count($coresByCore[$core] ?? []) > 1 ? self::REMARK_DUP_ORDER : '';
|
||||
|
||||
return [self::STATUS_MATCHED, round((float) $amounts[$fallbackKey], 2), $remark];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
}
|
||||
$shops = array_keys($identitiesByCore[$core] ?? []);
|
||||
if (count($shops) === 1) {
|
||||
$otherShop = (string) $shops[0];
|
||||
$otherIdentities = array_keys($identitiesByCore[$core][$otherShop] ?? []);
|
||||
if (count($otherIdentities) > 1) {
|
||||
return [self::STATUS_UNMATCHED, null, self::REMARK_DUP_ORDER];
|
||||
}
|
||||
if (count($otherIdentities) === 1) {
|
||||
$otherKey = $otherShop . '|' . $otherIdentities[0];
|
||||
if (isset($amounts[$otherKey])) {
|
||||
$remark = $otherShop === '' ? '' : self::REMARK_SHOP_DIFF;
|
||||
|
||||
if (isset($coreSeen[$core]) && count($coresByCore[$core] ?? []) > 1) {
|
||||
$sum = 0.0;
|
||||
$hasAmount = false;
|
||||
foreach (array_keys($coresByCore[$core]) as $otherShop) {
|
||||
$key2 = $otherShop . '|' . $core;
|
||||
if (isset($amounts[$key2])) {
|
||||
$sum += (float) $amounts[$key2];
|
||||
$hasAmount = true;
|
||||
return [self::STATUS_MATCHED, round((float) $amounts[$otherKey], 2), $remark];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
}
|
||||
}
|
||||
if ($hasAmount) {
|
||||
return [self::STATUS_MATCHED, round($sum, 2), self::REMARK_DUP_ORDER];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
}
|
||||
|
||||
if (isset($coreSeen[$core])) {
|
||||
// 订单侧只有店铺名为空的记录:按订单号兜底匹配。
|
||||
$blankKey = '|' . $core;
|
||||
if (isset($amounts[$blankKey])) {
|
||||
return [self::STATUS_MATCHED, round((float) $amounts[$blankKey], 2), ''];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, $noAmountRemark];
|
||||
// 同一核心号跨多个店铺或同一店铺多个完整单号:禁止把金额相加。
|
||||
return [self::STATUS_UNMATCHED, null, self::REMARK_DUP_ORDER];
|
||||
}
|
||||
|
||||
return [self::STATUS_UNMATCHED, null, self::REMARK_NO_ORDER];
|
||||
|
||||
@ -32,7 +32,7 @@ class StoreReceiptMemoryTest extends TestCase
|
||||
|
||||
return (new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'aggregateBills'))->invoke(
|
||||
new SyncStoreReceiptOrderMatches(), $rows, ['label' => '淘系支付宝'], 'online_order_no', 'occurred_at',
|
||||
'income_amount', 'expense_amount', 'online_real_store', $planting, $map, '淘宝',
|
||||
'income_amount', 'expense_amount', 'online_real_store', $planting, [], [], $map, '淘宝',
|
||||
);
|
||||
}
|
||||
|
||||
@ -40,10 +40,10 @@ class StoreReceiptMemoryTest extends TestCase
|
||||
{
|
||||
$bills = [
|
||||
$this->bill('优惠收入', '260630-10001', 0, 3),
|
||||
$this->bill('订单收入', '10001', 100, 0, ['occurred_at' => '2026-08-02 10:00:00']),
|
||||
$this->bill('订单收入', '10001', 25),
|
||||
$this->bill('冲减收入', '10001', 0, 2),
|
||||
$this->bill('退款', '10001', 0, 7),
|
||||
$this->bill('订单收入', '260630-10001', 100, 0, ['occurred_at' => '2026-08-02 10:00:00']),
|
||||
$this->bill('订单收入', '260630-10001', 25),
|
||||
$this->bill('冲减收入', '260630-10001', 0, 2),
|
||||
$this->bill('退款', '260630-10001', 0, 7),
|
||||
$this->bill('优惠收入', '20002', 4),
|
||||
$this->bill('退款', '20002', 0, 1),
|
||||
$this->bill('冲减收入', '', 0, 8),
|
||||
@ -56,7 +56,7 @@ class StoreReceiptMemoryTest extends TestCase
|
||||
$result = $this->aggregate((function () use ($bills) { yield from $bills; })(), ['10001' => true]);
|
||||
$key = SyncStoreReceiptOrderMatches::normalizeShop('天猫_测试店');
|
||||
$this->assertCount(2, $result['groups']);
|
||||
$paid = $result['groups'][$key . '|10001'];
|
||||
$paid = $result['groups'][$key . '|260630-10001'];
|
||||
$this->assertSame(2, $paid['payment_count']);
|
||||
$this->assertSame(125.0, $paid['income_amount']);
|
||||
$this->assertSame(-3.0, $paid['discount_amount']);
|
||||
@ -84,6 +84,55 @@ class StoreReceiptMemoryTest extends TestCase
|
||||
$this->assertSame(-2.0, $result['promoOnlyGroups'][$key . '||退款']['refund_amount']);
|
||||
}
|
||||
|
||||
public function test_same_core_with_different_date_prefixes_stays_as_two_orders(): void
|
||||
{
|
||||
$result = $this->aggregate([
|
||||
$this->bill('订单收入', '260627-475634091573139', 13.91),
|
||||
$this->bill('订单收入', '260713-475634091573139', 29.51),
|
||||
]);
|
||||
|
||||
$shop = SyncStoreReceiptOrderMatches::normalizeShop('天猫_测试店');
|
||||
$this->assertCount(2, $result['groups']);
|
||||
$first = $result['groups'][$shop . '|260627-475634091573139'];
|
||||
$second = $result['groups'][$shop . '|260713-475634091573139'];
|
||||
$this->assertSame('475634091573139', $first['order_core_no']);
|
||||
$this->assertSame('475634091573139', $second['order_core_no']);
|
||||
$this->assertSame('260627-475634091573139', $first['order_identity']);
|
||||
$this->assertSame('260713-475634091573139', $second['order_identity']);
|
||||
$this->assertSame(13.91, $first['income_amount']);
|
||||
$this->assertSame(29.51, $second['income_amount']);
|
||||
}
|
||||
|
||||
public function test_order_side_exact_full_number_wins_and_ambiguous_core_fallback_never_sums(): void
|
||||
{
|
||||
$command = new SyncStoreReceiptOrderMatches();
|
||||
$method = new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'matchOrderSide');
|
||||
$shop = SyncStoreReceiptOrderMatches::normalizeShop('拼多多-欧意朗家具官方旗舰店');
|
||||
$core = '475634091573139';
|
||||
$first = '260627-475634091573139';
|
||||
$second = '260713-475634091573139';
|
||||
$amounts = [
|
||||
$shop . '|' . $first => 13.91,
|
||||
$shop . '|' . $second => 29.51,
|
||||
];
|
||||
$orderSeen = [
|
||||
$shop . '|' . $first => true,
|
||||
$shop . '|' . $second => true,
|
||||
];
|
||||
$identitiesByCore = [$core => [$shop => [$first => true, $second => true]]];
|
||||
$coresByCore = [$core => [$shop => true]];
|
||||
|
||||
$exact = $method->invoke(
|
||||
$command, $first, $core, $shop, $amounts, $orderSeen, [$core => true], $coresByCore, $identitiesByCore, '无金额'
|
||||
);
|
||||
$this->assertSame(['已匹配', 13.91, ''], $exact);
|
||||
|
||||
$ambiguous = $method->invoke(
|
||||
$command, $core, $core, $shop, $amounts, $orderSeen, [$core => true], $coresByCore, $identitiesByCore, '无金额'
|
||||
);
|
||||
$this->assertSame(['未匹配', null, '订单侧同号多条'], $ambiguous);
|
||||
}
|
||||
|
||||
public function test_empty_order_promotions_remain_separate_even_with_an_empty_order_payment(): void
|
||||
{
|
||||
$result = $this->aggregate([
|
||||
@ -134,7 +183,7 @@ class StoreReceiptMemoryTest extends TestCase
|
||||
$result = (new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'aggregateBills'))->invoke(
|
||||
new SyncStoreReceiptOrderMatches(), [(object) ['store_name' => '京东-测试店', 'order_no' => '70007',
|
||||
'bill_date' => '2026-08-09', 'income' => 12.5, 'bill_item' => '货款(收)']],
|
||||
['label' => '京东'], 'order_no', 'bill_date', 'income', '', '', [], $map, '京东',
|
||||
['label' => '京东'], 'order_no', 'bill_date', 'income', '', '', [], [], [], $map, '京东',
|
||||
);
|
||||
$group = reset($result['groups']);
|
||||
$this->assertSame(12.5, $result['incomeTotal']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user