diff --git a/app/Console/Commands/PreviewStoreReceiptDefaultCompensation.php b/app/Console/Commands/PreviewStoreReceiptDefaultCompensation.php index feb006e6..c77c0a98 100644 --- a/app/Console/Commands/PreviewStoreReceiptDefaultCompensation.php +++ b/app/Console/Commands/PreviewStoreReceiptDefaultCompensation.php @@ -13,7 +13,7 @@ class PreviewStoreReceiptDefaultCompensation extends Command {mapping : 最新科目映射表 xlsx 路径} {--output= : 输出 JSON 路径;默认写入 storage/app/local/performance-details/default-compensation}'; - protected $description = '本地预览:按最新科目映射表生成业绩明细「违约赔付(冲减收入)」金额快照,不写数据库'; + protected $description = '人工核对:按指定科目映射表生成「违约赔付(冲减收入)」金额快照,不写数据库'; public function handle(StoreReceiptDefaultCompensationSnapshotBuilder $builder): int { diff --git a/app/Console/Commands/SyncStoreReceiptOrderMatches.php b/app/Console/Commands/SyncStoreReceiptOrderMatches.php index 2c512a78..36eebac9 100644 --- a/app/Console/Commands/SyncStoreReceiptOrderMatches.php +++ b/app/Console/Commands/SyncStoreReceiptOrderMatches.php @@ -115,18 +115,19 @@ class SyncStoreReceiptOrderMatches extends Command * 「优惠金额」列口径:账单的总账备注命中以下取值。 * * 总账备注优先取「财务规则 › 科目映射表」的财务备注(与店铺收款账户账单页一致), - * 取不到时回落到 ec_account_ledger_remarks。金额不计入收入金额与差额。 + * 取不到时回落到 ec_account_ledger_remarks。金额不计入收入,但计入差额公式。 */ private const DISCOUNT_REMARKS = [ '优惠收入', ]; - /** - * 「冲减金额」列口径:账单的总账备注命中以下取值,不参与收入金额与差额计算。 - * 「违约赔付(冲减收入)」是冲减收入的细分项,总额仍计入冲减金额。 - */ + /** 「冲减金额」列口径:只认普通冲减收入,不包含违约赔付。 */ private const OFFSET_REMARKS = [ '冲减收入', + ]; + + /** 「违约赔付(冲减收入)」独立列口径,计算差额时单独加回一次。 */ + private const DEFAULT_COMPENSATION_REMARKS = [ '违约赔付(冲减收入)', ]; @@ -278,16 +279,17 @@ class SyncStoreReceiptOrderMatches extends Command // 「总账备注」→ 按平台分组的账单项目集合(淘系页签同页混有淘宝/天猫/阿里巴巴店铺, // 必须连店铺名前缀一起过滤,才能与页面按行取到的总账备注一致)。 - // 收入金额只认 LEDGER_REMARKS;优惠/冲减金额各自单独成列,不参与收入与差额。 + // 收入金额只认 LEDGER_REMARKS;优惠、冲减、违约赔付各自单独成列。 $baseGroups = $this->ledgerRemarkGroups($platform); $discountGroups = $this->ledgerRemarkGroups($platform, self::DISCOUNT_REMARKS); $offsetGroups = $this->ledgerRemarkGroups($platform, self::OFFSET_REMARKS); + $defaultCompensationGroups = $this->ledgerRemarkGroups($platform, self::DEFAULT_COMPENSATION_REMARKS); $refundGroups = $this->ledgerRemarkGroups($platform, self::REFUND_REMARKS); - // 总账备注现在优先取科目映射表的财务备注,因此这两列的候选账单项目 - // 也要按科目映射表补一遍,否则「优惠收入 / 冲减收入 / 退款」永远取不到项目。 + // 总账备注优先取科目映射表的财务备注,因此候选账单项目也要按映射表补一遍。 $promoItems = array_values(array_unique(array_merge( $this->accountMappingItems($platform, self::DISCOUNT_REMARKS), $this->accountMappingItems($platform, self::OFFSET_REMARKS), + $this->accountMappingItems($platform, self::DEFAULT_COMPENSATION_REMARKS), $this->accountMappingItems($platform, self::REFUND_REMARKS), ))); // 微信账户页签的账单项目带「wx」前缀(wx限时红包代商家垫付扣回 等), @@ -299,8 +301,12 @@ class SyncStoreReceiptOrderMatches extends Command return self::FAILURE; } $this->info('总账备注命中平台分组:' . count($baseGroups) . ' 组'); - if ($discountGroups === [] && $offsetGroups === [] && $refundGroups === [] && $promoItems === []) { - $this->warn('「优惠收入」「冲减收入」「退款」在科目映射表与总账备注映射里都没有对应账单项目,三列将全部为空。'); + if ($discountGroups === [] + && $offsetGroups === [] + && $defaultCompensationGroups === [] + && $refundGroups === [] + && $promoItems === []) { + $this->warn('「优惠收入」「冲减收入」「违约赔付(冲减收入)」「退款」都没有对应账单项目,相关列将全部为空。'); } $select = ['store_name', 'bill_item', $orderColumn, $timeColumn, $amountColumn]; @@ -355,10 +361,10 @@ class SyncStoreReceiptOrderMatches extends Command $billQuery->whereIn('generation', $generations); $this->info('活动代:' . count($generations) . ' 个'); } - $billQuery->where(function ($group) use ($baseGroups, $discountGroups, $offsetGroups, $refundGroups, $promoItems): void { + $billQuery->where(function ($group) use ($baseGroups, $discountGroups, $offsetGroups, $defaultCompensationGroups, $refundGroups, $promoItems): void { LedgerRemarkFilter::applyGroups( $group, - array_merge($baseGroups, $discountGroups, $offsetGroups, $refundGroups), + array_merge($baseGroups, $discountGroups, $offsetGroups, $defaultCompensationGroups, $refundGroups), 'bill_item', static fn ($sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'), static fn ($sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'), @@ -380,9 +386,11 @@ class SyncStoreReceiptOrderMatches extends Command [ 'groups' => $groups, 'promoOnlyGroups' => $promoOnlyGroups, 'paymentTotal' => $paymentTotal, 'incomeTotal' => $incomeTotal, - 'discountTotal' => $discountTotal, 'offsetTotal' => $offsetTotal, 'refundTotal' => $refundTotal, + 'discountTotal' => $discountTotal, 'offsetTotal' => $offsetTotal, + 'defaultCompensationTotal' => $defaultCompensationTotal, 'refundTotal' => $refundTotal, 'moneyRemarkRows' => $moneyRemarkRows, 'attachedDiscount' => $attachedDiscount, - 'attachedOffset' => $attachedOffset, 'unattachedRows' => $unattachedRows, 'billCount' => $billCount, + 'attachedOffset' => $attachedOffset, 'attachedDefaultCompensation' => $attachedDefaultCompensation, + 'unattachedRows' => $unattachedRows, 'billCount' => $billCount, ] = $this->aggregateBills( $this->streamBillRows($billQuery->orderBy($timeColumn)), $source, $orderColumn, $timeColumn, $amountColumn, $expenseColumn, $realStoreColumn, @@ -400,7 +408,12 @@ class SyncStoreReceiptOrderMatches extends Command $plantingUnreimbursedTotal = 0.0; $plantingUnreimbursedRows = 0; $this->info('账单收款笔数:' . $paymentTotal . ',按「店铺 + 订单号」聚合后:' . count($groups) . ' 行'); - $this->line(' 优惠金额合计:' . number_format($discountTotal, 2) . ',冲减金额合计:' . number_format($offsetTotal, 2) . ',退款金额合计:' . number_format($refundTotal, 2)); + $this->line( + ' 优惠金额合计:' . number_format($discountTotal, 2) + . ',冲减金额合计:' . number_format($offsetTotal, 2) + . ',违约赔付(冲减收入)合计:' . number_format($defaultCompensationTotal, 2) + . ',退款金额合计:' . number_format($refundTotal, 2) + ); foreach ($groups as $group) { if (! empty($group['planting_seen'])) { $plantingRows++; @@ -431,11 +444,16 @@ class SyncStoreReceiptOrderMatches extends Command number_format($plantingUnreimbursedTotal, 2), )); $this->line(sprintf( - ' 已挂订单行:优惠 %s / 冲减 %s;未挂到订单行:%s(金额 %s,已补成「无收款账单 / 账单无单号」行)', + ' 已挂订单行:优惠 %s / 冲减 %s / 违约赔付 %s;未挂到订单行:%s(金额 %s,已补成「无收款账单 / 账单无单号」行)', number_format($attachedDiscount, 2), number_format($attachedOffset, 2), + number_format($attachedDefaultCompensation, 2), number_format($unattachedRows) . ' 行', - number_format(($discountTotal + $offsetTotal) - ($attachedDiscount + $attachedOffset), 2), + number_format( + ($discountTotal + $offsetTotal + $defaultCompensationTotal) + - ($attachedDiscount + $attachedOffset + $attachedDefaultCompensation), + 2 + ), )); unset($plantingCores); if ((bool) $this->option('amounts-only')) { @@ -712,9 +730,14 @@ class SyncStoreReceiptOrderMatches extends Command $remark = self::REMARK_MULTI_PAY; } - // 差额口径:(收入金额 + 优惠金额 + 冲减金额)− 比对基准金额(默认订单实付金额)。 - // 优惠/冲减属于同一订单的账单,把它们加回来,差额才只反映真正对不上的部分。 - $promoAmount = round((float) ($group['discount_amount'] ?? 0.0) + (float) ($group['offset_amount'] ?? 0.0), 2); + // 差额口径:(收入 + 优惠 + 冲减 + 违约赔付)− 比对基准金额。 + // 三类抵减项分别展示,但计算差额时都必须且只能加回一次。 + $promoAmount = round( + (float) ($group['discount_amount'] ?? 0.0) + + (float) ($group['offset_amount'] ?? 0.0) + + (float) ($group['default_compensation_amount'] ?? 0.0), + 2 + ); $difference = $basisAmount === null ? null : round($income + $promoAmount - $basisAmount, 2); // 双口径兜底:账单(收入+优惠+冲减)能对上「卖家实收」时,按实收核对—— @@ -801,6 +824,9 @@ class SyncStoreReceiptOrderMatches extends Command 'income_amount' => $income, 'discount_amount' => ! empty($group['discount_seen']) ? round((float) $group['discount_amount'], 2) : null, 'offset_amount' => ! empty($group['offset_seen']) ? round((float) $group['offset_amount'], 2) : null, + 'default_compensation_amount' => ! empty($group['default_compensation_seen']) + ? round((float) $group['default_compensation_amount'], 2) + : null, 'refund_amount' => ! empty($group['refund_seen']) ? round((float) $group['refund_amount'], 2) : null, 'planting_amount' => ! empty($group['planting_seen']) ? $income : null, 'planting_label' => self::plantingLabel($group), @@ -855,21 +881,20 @@ class SyncStoreReceiptOrderMatches extends Command $this->line(' 订单侧实付金额合计:' . number_format($paidTotal, 2)); $this->line(' 差额合计:' . number_format($differenceTotal, 2)); - // 重建后自检:本月账单里确实存在「优惠收入 / 冲减收入 / 退款」的账单行,但重建出来的行 - // 三列一行都没写,说明这次重建用的代码或参数不对(历史上出现过用旧代码重建、把优惠金额 - // 整列写成空),这里显式报错,避免静默丢列后没人发现。 + // 重建后自检:相关账单行存在时,至少应有一列被写入,避免旧代码静默丢列。 $promoFilled = (int) DB::connection($connection)->table(self::TARGET_TABLE) ->where('report_month', $reportMonth) ->where('platform', (string) $source['label']) ->where(function ($query): void { $query->whereNotNull('discount_amount') ->orWhereNotNull('offset_amount') + ->orWhereNotNull('default_compensation_amount') ->orWhereNotNull('refund_amount'); }) ->count(); if ($promoItems !== [] && $promoFilled === 0 && $moneyRemarkRows > 0) { $this->error(sprintf( - '重建后自检不通过:%s %s 的「优惠金额 / 冲减金额 / 退款金额」三列一行都没写,但本月账单里有 %d 行命中优惠/冲减/退款(科目映射表里有 %d 个相关账单项目)。' + '重建后自检不通过:%s %s 的优惠、冲减、违约赔付、退款金额一行都没写,但本月账单里有 %d 行命中了相关备注(科目映射表里有 %d 个相关账单项目)。' . '请确认使用最新代码(优惠金额逻辑见 c8a035a1)重跑,或补跑 store-receipt:order-match:sync --amounts-only。', $month, (string) $source['label'], @@ -987,8 +1012,8 @@ class SyncStoreReceiptOrderMatches extends Command } /** - * 只回填「优惠金额 / 冲减金额 / 种菜金额」三列:不重建业绩明细、不查订单侧, - * 先清空该账期该平台的三列,再按 row_key 更新镜像里已存在的行。 + * 只回填优惠、冲减、违约赔付、退款、种菜等金额列:不重建业绩明细、不查订单侧, + * 先清空该账期该平台的相关列,再按 row_key 更新镜像里已存在的行。 * * @param array $source * @param array> $groups @@ -1018,6 +1043,7 @@ class SyncStoreReceiptOrderMatches extends Command ->update([ 'discount_amount' => null, 'offset_amount' => null, + 'default_compensation_amount' => null, 'refund_amount' => null, 'planting_amount' => null, 'planting_label' => null, @@ -1025,12 +1051,14 @@ class SyncStoreReceiptOrderMatches extends Command $discounts = []; $offsets = []; + $defaultCompensations = []; $refunds = []; $plantings = []; $plantingLabels = []; foreach ($groups as $group) { if (empty($group['discount_seen']) && empty($group['offset_seen']) + && empty($group['default_compensation_seen']) && empty($group['refund_seen']) && empty($group['planting_seen']) && empty($group['planting_reimbursement_seen']) @@ -1054,6 +1082,9 @@ class SyncStoreReceiptOrderMatches extends Command if (! empty($group['offset_seen'])) { $offsets[$rowKey] = round((float) $group['offset_amount'], 2); } + if (! empty($group['default_compensation_seen'])) { + $defaultCompensations[$rowKey] = round((float) $group['default_compensation_amount'], 2); + } if (! empty($group['refund_seen'])) { $refunds[$rowKey] = round((float) $group['refund_amount'], 2); } @@ -1069,6 +1100,7 @@ class SyncStoreReceiptOrderMatches extends Command $this->updateColumnValues($connection, $reportMonth, 'discount_amount', $discounts); $this->updateColumnValues($connection, $reportMonth, 'offset_amount', $offsets); + $this->updateColumnValues($connection, $reportMonth, 'default_compensation_amount', $defaultCompensations); $this->updateColumnValues($connection, $reportMonth, 'refund_amount', $refunds); $this->updateColumnValues($connection, $reportMonth, 'planting_amount', $plantings); $this->updateColumnValues($connection, $reportMonth, 'planting_label', $plantingLabels); @@ -1104,17 +1136,25 @@ class SyncStoreReceiptOrderMatches extends Command ->where('platform', $platform) ->whereNotNull($basisColumn) ->update([ - 'difference' => DB::raw('ROUND(income_amount + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0) - ' . $basisColumn . ', 2)'), + 'difference' => DB::raw( + 'ROUND(income_amount' + . ' + COALESCE(discount_amount, 0)' + . ' + COALESCE(offset_amount, 0)' + . ' + COALESCE(default_compensation_amount, 0)' + . ' - ' . $basisColumn . ', 2)' + ), ]); $this->info(sprintf( - '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 补「无收款账单/无单号」优惠冲减行 %d 行 / 种菜 %d 行(%s)/ 标签「种菜报销」%d 行 / 标签「种菜未报销」%d 行,镜像原有 %d 行,未重建业绩明细。', + '已回填 %s %s:优惠 %d 行(%s)/ 冲减 %d 行(%s)/ 违约赔付 %d 行(%s)/ 补「无收款账单/无单号」抵减行 %d 行 / 种菜 %d 行(%s)/ 标签「种菜报销」%d 行 / 标签「种菜未报销」%d 行,镜像原有 %d 行,未重建业绩明细。', $month, $platform, count($discounts), number_format(array_sum($discounts), 2), count($offsets), number_format(array_sum($offsets), 2), + count($defaultCompensations), + number_format(array_sum($defaultCompensations), 2), $promoRows, count($plantings), number_format(array_sum($plantings), 2), @@ -1181,6 +1221,7 @@ class SyncStoreReceiptOrderMatches extends Command $incomeTotal = 0.0; $discountTotal = 0.0; $offsetTotal = 0.0; + $defaultCompensationTotal = 0.0; $refundTotal = 0.0; /** @var array $promoRowsByKey 门店+订单 维度的优惠/冲减账单行数(用于统计未挂单行数) */ $promoRowsByKey = []; @@ -1203,7 +1244,8 @@ class SyncStoreReceiptOrderMatches extends Command $this->billGoodsName($bill), ); $isPromoRemark = in_array($remark, self::DISCOUNT_REMARKS, true) - || in_array($remark, self::OFFSET_REMARKS, true); + || in_array($remark, self::OFFSET_REMARKS, true) + || in_array($remark, self::DEFAULT_COMPENSATION_REMARKS, true); $isRefundRemark = in_array($remark, self::REFUND_REMARKS, true); $isIncomeRemark = in_array($remark, self::LEDGER_REMARKS, true); if ($isPromoRemark || $isRefundRemark) { @@ -1213,6 +1255,7 @@ class SyncStoreReceiptOrderMatches extends Command $isIncomeRemark => 'income', $isRefundRemark => 'refund', in_array($remark, self::DISCOUNT_REMARKS, true) => 'discount', + in_array($remark, self::DEFAULT_COMPENSATION_REMARKS, true) => 'default_compensation', in_array($remark, self::OFFSET_REMARKS, true) => 'offset', default => null, }; @@ -1243,6 +1286,8 @@ class SyncStoreReceiptOrderMatches extends Command 'discount_seen' => false, 'offset_amount' => 0.0, 'offset_seen' => false, + 'default_compensation_amount' => 0.0, + 'default_compensation_seen' => false, 'refund_amount' => 0.0, 'refund_seen' => false, ]; @@ -1269,6 +1314,8 @@ class SyncStoreReceiptOrderMatches extends Command 'discount_seen' => false, 'offset_amount' => 0.0, 'offset_seen' => false, + 'default_compensation_amount' => 0.0, + 'default_compensation_seen' => false, 'refund_amount' => 0.0, 'refund_seen' => false, ]; @@ -1290,6 +1337,11 @@ class SyncStoreReceiptOrderMatches extends Command $groups[$key]['refund_seen'] = true; $groups[$key]['refund_amount'] += $amount; $refundTotal += $amount; + } elseif ($bucket === 'default_compensation') { + $groups[$key]['default_compensation_seen'] = true; + $groups[$key]['default_compensation_amount'] += $amount; + $defaultCompensationTotal += $amount; + $promoRowsByKey[$key] = ($promoRowsByKey[$key] ?? 0) + 1; } else { $groups[$key]['offset_seen'] = true; $groups[$key]['offset_amount'] += $amount; @@ -1317,9 +1369,11 @@ class SyncStoreReceiptOrderMatches extends Command // 口径对照:账单口径合计、已挂到订单行的合计、挂不上的行数(账单没有订单号 / 订单没有收款行)。 $attachedDiscount = 0.0; $attachedOffset = 0.0; + $attachedDefaultCompensation = 0.0; foreach ($groups as $group) { $attachedDiscount += (float) $group['discount_amount']; $attachedOffset += (float) $group['offset_amount']; + $attachedDefaultCompensation += (float) $group['default_compensation_amount']; } $unattachedRows = 0; foreach ($promoRowsByKey as $key => $count) { @@ -1336,7 +1390,9 @@ class SyncStoreReceiptOrderMatches extends Command return compact( 'groups', 'promoOnlyGroups', 'paymentTotal', 'incomeTotal', 'discountTotal', - 'offsetTotal', 'refundTotal', 'moneyRemarkRows', 'attachedDiscount', 'attachedOffset', 'unattachedRows', 'billCount', + 'offsetTotal', 'defaultCompensationTotal', 'refundTotal', 'moneyRemarkRows', + 'attachedDiscount', 'attachedOffset', 'attachedDefaultCompensation', + 'unattachedRows', 'billCount', ); } @@ -1366,6 +1422,9 @@ class SyncStoreReceiptOrderMatches extends Command 'income_amount' => 0, 'discount_amount' => ! empty($group['discount_seen']) ? round((float) $group['discount_amount'], 2) : null, 'offset_amount' => ! empty($group['offset_seen']) ? round((float) $group['offset_amount'], 2) : null, + 'default_compensation_amount' => ! empty($group['default_compensation_seen']) + ? round((float) $group['default_compensation_amount'], 2) + : null, 'refund_amount' => ! empty($group['refund_seen']) ? round((float) $group['refund_amount'], 2) : null, // 补行没有订单侧商品金额,种菜金额同样留空;列集合与顺序必须与主行完全一致,批量插入才不会错列。 'planting_amount' => null, diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 26dfef09..2ef11902 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -197,6 +197,19 @@ class Pages extends Controller ]; } + /** 业绩明细三类抵减项的 SQL 合计。 */ + private static function receiptAdjustmentSql(): string + { + return 'COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0)' + . ' + COALESCE(default_compensation_amount, 0)'; + } + + /** 业绩明细净收入 SQL:收入加三类抵减项。 */ + private static function receiptNetSql(): string + { + return 'COALESCE(income_amount, 0) + (' . self::receiptAdjustmentSql() . ')'; + } + /** * 「订单实付含已退款部分」的聚水潭证据 SQL(仅抖音负差额行计算,CASE 守卫保证其它行不触发子查询)。 * @@ -209,7 +222,7 @@ class Pages extends Controller */ private static function receiptRefundEvidenceSql(): array { - $guard = "platform = '抖音' AND COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0)) < -0.005"; + $guard = "platform = '抖音' AND COALESCE(difference, " . self::receiptNetSql() . ') < -0.005'; $refundWhere = "o.so_id = store_receipt_order_match_rows.online_order_no AND rf.refund > 0 AND rf.shop_status LIKE '%SUCCESS%' AND COALESCE(rf.status, '') NOT LIKE '%Split%'"; @@ -237,7 +250,7 @@ class Pages extends Controller */ private static function receiptDewuMarketingEvidenceSql(): array { - $guard = "platform = '得物' AND COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0)) < -0.005"; + $guard = "platform = '得物' AND COALESCE(difference, " . self::receiptNetSql() . ') < -0.005'; return [ 'guard' => $guard, @@ -258,7 +271,7 @@ class Pages extends Controller */ private static function receiptJingxiSupplyEvidenceSql(): array { - $guard = "platform = '京喜账单' AND COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0)) > 0.005"; + $guard = "platform = '京喜账单' AND COALESCE(difference, " . self::receiptNetSql() . ') > 0.005'; // 京喜账单按「活动代」保留多代历史导入:只认与本月重叠的活动代(与同步命令同一口径)。 // 不按代过滤时,旧代 + 新代会把同一笔直营服务费算两遍(例如 3,432 被算成 6,864), // 供货价规则整月对不上,全部落进「差额待核实」。没有活动代登记的月份保持原口径。 @@ -312,7 +325,8 @@ class Pages extends Controller $label = $basis['label']; $rules = []; - $effective = 'COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))'; + $adjustment = self::receiptAdjustmentSql(); + $effective = 'COALESCE(difference, ' . self::receiptNetSql() . ')'; $rules[$label . '未匹配'] = "match_status = '未匹配'"; $rules[$label . '取数为0(种菜)'] = "match_status = '已匹配' AND planting_amount IS NOT NULL AND ABS(COALESCE($column, 0)) < 0.005"; $rules[$label . '取数为0'] = "match_status = '已匹配' AND planting_amount IS NULL AND ABS(COALESCE($column, 0)) < 0.005"; @@ -381,17 +395,20 @@ class Pages extends Controller // C 方案:这三条是"通用等式档"——口径(卖家实收 / 应付已付)能对上账单的行让位给口径兜底 // (归零 + 口径标识),只有口径也对不上时才显示这三档,避免把口径差标成一堆"未减优惠/冲减"。 $notBasisMarked = self::receiptReasonNotBasisMarkedSql(); - $rules[$label . '未减冲减'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0) + COALESCE(discount_amount, 0))) < 0.005 AND " . $notBasisMarked; - $rules[$label . '未减优惠'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0) + COALESCE(offset_amount, 0))) < 0.005 AND " . $notBasisMarked; + $rules[$label . '未减冲减'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0)" + . ' + COALESCE(discount_amount, 0) + COALESCE(default_compensation_amount, 0))) < 0.005 AND ' . $notBasisMarked; + $rules[$label . '未减优惠'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0)" + . ' + COALESCE(offset_amount, 0) + COALESCE(default_compensation_amount, 0))) < 0.005 AND ' . $notBasisMarked; $pddPaymentDiscountGuard = "NOT (platform = '拼多多' AND match_status = '已匹配'" . ' AND ' . $effective . ' > 0.005' . ' AND COALESCE(refund_amount, 0) < -0.005' - . ' AND (' . $effective . ' - COALESCE(discount_amount, 0) - COALESCE(offset_amount, 0)) > 0.005' - . ' AND (' . $effective . ' - COALESCE(discount_amount, 0) - COALESCE(offset_amount, 0)) <= 20.005)'; + . ' AND (' . $effective . ' - (' . $adjustment . ')) > 0.005' + . ' AND (' . $effective . ' - (' . $adjustment . ')) <= 20.005)'; $rules[$label . '未减优惠和冲减'] = "match_status = '已匹配'" . ' AND (ABS(COALESCE(' . $column . ', 0) - COALESCE(income_amount, 0)) < 0.005' - . ' OR ((ABS(COALESCE(discount_amount, 0)) > 0.005 OR ABS(COALESCE(offset_amount, 0)) > 0.005)' - . ' AND ABS(' . $effective . ' - (COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))) < 0.02' + . ' OR ((ABS(COALESCE(discount_amount, 0)) > 0.005 OR ABS(COALESCE(offset_amount, 0)) > 0.005' + . ' OR ABS(COALESCE(default_compensation_amount, 0)) > 0.005)' + . ' AND ABS(' . $effective . ' - (' . $adjustment . ')) < 0.02' . ' AND ' . $pddPaymentDiscountGuard . '))' . ' AND ' . $notBasisMarked; @@ -429,14 +446,14 @@ class Pages extends Controller . ') ELSE 0 END = 1) OR ' . $taoRefundHit . ' OR ' . $pddFullRefundHit . ' OR (ABS(COALESCE(refund_amount, 0)) > 0.005' . ' AND (ABS(' . $effective . ' - COALESCE(refund_amount, 0)) < 0.02' - . ' OR ((ABS(COALESCE(discount_amount, 0)) > 0.005 OR ABS(COALESCE(offset_amount, 0)) > 0.005)' - . ' AND ABS(' . $effective . ' - (COALESCE(refund_amount, 0)' - . ' + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))) < 0.02))' + . ' OR ((ABS(COALESCE(discount_amount, 0)) > 0.005 OR ABS(COALESCE(offset_amount, 0)) > 0.005' + . ' OR ABS(COALESCE(default_compensation_amount, 0)) > 0.005)' + . ' AND ABS(' . $effective . ' - (COALESCE(refund_amount, 0) + (' . $adjustment . '))) < 0.02))' . ' AND ' . $notBasisMarked . ')'; // 拼多多部分退款:账单收入高于聚水潭买家实付,且扣除优惠/冲减后仍有正向小额差额, // 与「多多支付立减」等平台支付类补贴一致;只标注原因,不改差额和收入。 - $pddPaymentDiscount = '(' . $effective . ' - COALESCE(discount_amount, 0) - COALESCE(offset_amount, 0))'; + $pddPaymentDiscount = '(' . $effective . ' - (' . $adjustment . '))'; $rules['差额=平台支付立减'] = "platform = '拼多多'" . " AND match_status = '已匹配'" . ' AND ' . $effective . ' > 0.005' @@ -453,6 +470,7 @@ class Pages extends Controller . ' AND ' . $effective . ' < -0.005' . ' AND ABS(COALESCE(discount_amount, 0)) < 0.005' . ' AND ABS(COALESCE(offset_amount, 0)) < 0.005' + . ' AND ABS(COALESCE(default_compensation_amount, 0)) < 0.005' . ' AND ABS(COALESCE(refund_amount, 0)) < 0.005' . ' AND COALESCE(paid_amount, 0) > COALESCE(income_amount, 0) + 0.005' . ' AND ' . $notBasisMarked; @@ -477,7 +495,7 @@ class Pages extends Controller */ private static function receiptDifferenceRemarkIndexSql(): string { - $effective = 'COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))'; + $effective = 'COALESCE(difference, ' . self::receiptNetSql() . ')'; $sql = 'CASE WHEN ABS(' . $effective . ') <= 0.005 THEN 0'; $index = 1; foreach (self::receiptDifferenceRemarkBaseRules() as $condition) { @@ -586,7 +604,7 @@ class Pages extends Controller /** 未命中复杂原因时,差额的默认口径。 */ private static function receiptDefaultDifferenceSql(): string { - return 'COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))'; + return 'COALESCE(difference, ' . self::receiptNetSql() . ')'; } /** @@ -614,7 +632,10 @@ class Pages extends Controller $label = $basis['label']; $difference = $row->difference === null - ? (float) ($row->income_amount ?? 0) + (float) ($row->discount_amount ?? 0) + (float) ($row->offset_amount ?? 0) + ? (float) ($row->income_amount ?? 0) + + (float) ($row->discount_amount ?? 0) + + (float) ($row->offset_amount ?? 0) + + (float) ($row->default_compensation_amount ?? 0) : (float) $row->difference; if (abs($difference) < 0.005) { @@ -630,6 +651,8 @@ class Pages extends Controller $income = (float) ($row->income_amount ?? 0); $discount = (float) ($row->discount_amount ?? 0); $offset = (float) ($row->offset_amount ?? 0); + $defaultCompensation = (float) ($row->default_compensation_amount ?? 0); + $adjustment = $discount + $offset + $defaultCompensation; if (abs($product) < 0.005) { return ($row->planting_amount ?? null) !== null ? $label . '取数为0(种菜)' : $label . '取数为0'; @@ -697,10 +720,10 @@ class Pages extends Controller ['订单实付按卖家实收核对', '订单实付按应付/已付核对'], true ); - if (! $basisMarked && abs($product - ($income + $discount)) < 0.005) { + if (! $basisMarked && abs($product - ($income + $discount + $defaultCompensation)) < 0.005) { return $label . '未减冲减'; } - if (! $basisMarked && abs($product - ($income + $offset)) < 0.005) { + if (! $basisMarked && abs($product - ($income + $offset + $defaultCompensation)) < 0.005) { return $label . '未减优惠'; } if (! $basisMarked && abs($product - $income) < 0.005) { @@ -746,7 +769,7 @@ class Pages extends Controller } // 拼多多部分退款:差额扣除优惠/冲减后,剩余部分与平台支付立减一致。 - $pddPaymentDiscount = $difference - $discount - $offset; + $pddPaymentDiscount = $difference - $adjustment; if (($row->platform ?? '') === '拼多多' && ! $basisMarked && $difference > 0.005 @@ -770,6 +793,7 @@ class Pages extends Controller && self::is1688StoreName((string) ($row->store_name ?? '')) && abs($discount) < 0.005 && abs($offset) < 0.005 + && abs($defaultCompensation) < 0.005 && abs((float) ($row->refund_amount ?? 0)) < 0.005 && (float) ($row->paid_amount ?? 0) > $income + 0.005) { return '订单实付含平台红包补贴'; @@ -782,9 +806,9 @@ class Pages extends Controller $refund = (float) ($row->refund_amount ?? 0); $hasRefund = abs($refund) > 0.005; - $hasDiscountOffset = abs($discount) > 0.005 || abs($offset) > 0.005; + $hasDiscountOffset = abs($discount) > 0.005 || abs($offset) > 0.005 || abs($defaultCompensation) > 0.005; if ($hasRefund && $hasDiscountOffset - && abs($difference - ($refund + $discount + $offset)) < 0.02) { + && abs($difference - ($refund + $adjustment)) < 0.02) { return '订单实付含已退款部分'; } if ($hasRefund && abs($difference - $refund) < 0.02) { @@ -792,7 +816,7 @@ class Pages extends Controller } if ( $hasDiscountOffset - && abs($difference - ($discount + $offset)) < 0.02 + && abs($difference - $adjustment) < 0.02 ) { return '订单实付未减优惠和冲减'; } @@ -1329,8 +1353,6 @@ class Pages extends Controller $unmatchedReasonFilter = $this->receiptOrderMatchFilterValues(request('unmatched_reason')); $differenceState = $this->receiptOrderMatchFilterValues(request('difference_state')); $orderNo = trim((string) request('online_order_no', '')); - $defaultCompensationAmounts = self::localReceiptDefaultCompensationAmounts($month); - // 闭包里要用 $this->applyReceiptRemarkFilter()(差异原因备注按公式判定),所以不能声明为 static $applyFilters = function ($query) use ($platformFilter, $storeFilter, $orderNo, $statusFilter, $remarkFilter, $unmatchedReasonFilter, $differenceState): void { if ($platformFilter !== []) { @@ -1375,6 +1397,7 @@ class Pages extends Controller SUM(income_amount) as income_total, SUM(discount_amount) as discount_total, SUM(offset_amount) as offset_total, + SUM(default_compensation_amount) as default_compensation_total, SUM(refund_amount) as refund_total, SUM(planting_amount) as planting_total, SUM(product_amount) as goods_total, @@ -1411,6 +1434,7 @@ class Pages extends Controller 'income_total' => round((float) ($agg->income_total ?? 0), 2), 'discount_total' => round((float) ($agg->discount_total ?? 0), 2), 'offset_total' => round((float) ($agg->offset_total ?? 0), 2), + 'default_compensation_total' => round((float) ($agg->default_compensation_total ?? 0), 2), 'refund_total' => round((float) ($agg->refund_total ?? 0), 2), 'planting_total' => round((float) ($agg->planting_total ?? 0), 2), 'goods_total' => round((float) ($agg->goods_total ?? 0), 2), @@ -1481,9 +1505,8 @@ class Pages extends Controller // 差额异常备注列:只展示「差额为什么产生」的原因备注;未匹配原因单独一列; // 未匹配行(差额为空)按比对基准金额=0 计入差额。 - $items = $items->map(static function (object $row) use ($defaultCompensationAmounts): object { + $items = $items->map(static function (object $row): object { $rawRemark = trim((string) ($row->remark ?? '')); - $row->default_compensation_amount = $defaultCompensationAmounts[(string) ($row->row_key ?? '')] ?? null; // 未匹配原因列:未匹配行照旧;「订单实付按卖家实收核对」这类口径标识在已匹配行同样展示。 $row->unmatched_reason = $rawRemark === '' ? null @@ -1493,7 +1516,10 @@ class Pages extends Controller if ($row->difference === null) { $row->difference = round( - (float) ($row->income_amount ?? 0) + (float) ($row->discount_amount ?? 0) + (float) ($row->offset_amount ?? 0), + (float) ($row->income_amount ?? 0) + + (float) ($row->discount_amount ?? 0) + + (float) ($row->offset_amount ?? 0) + + (float) ($row->default_compensation_amount ?? 0), 2 ); } @@ -1656,44 +1682,6 @@ class Pages extends Controller return array_keys($clean); } - /** - * 本地预览快照:读取按最新科目映射表生成的「违约赔付(冲减收入)」金额。 - * 文件不存在或内容无效时返回空数组,不影响正式页面。 - * - * @return array - */ - private static function localReceiptDefaultCompensationAmounts(?string $month): array - { - static $snapshots = []; - - $month = substr(trim((string) $month), 0, 7); - if (preg_match('/^\d{4}-\d{2}$/', $month) !== 1) { - return []; - } - if (isset($snapshots[$month])) { - return $snapshots[$month]; - } - - $path = storage_path('app/local/performance-details/default-compensation/' . $month . '.json'); - if (! is_file($path)) { - return $snapshots[$month] = []; - } - - $decoded = json_decode((string) file_get_contents($path), true); - if (! is_array($decoded) || ! is_array($decoded['amounts'] ?? null)) { - return $snapshots[$month] = []; - } - - $amounts = []; - foreach ($decoded['amounts'] as $rowKey => $amount) { - if (is_string($rowKey) && $rowKey !== '' && is_numeric($amount)) { - $amounts[$rowKey] = round((float) $amount, 2); - } - } - - return $snapshots[$month] = $amounts; - } - /** * 业绩明细的账期集合与当前账期:未指定月份时取镜像里最新的月份。 * @@ -2133,8 +2121,6 @@ class Pages extends Controller ]; [, $month] = $this->receiptOrderMatchMonths(); - $defaultCompensationAmounts = self::localReceiptDefaultCompensationAmounts($month); - $query = DB::connection($connection)->table('store_receipt_order_match_rows') ->whereNotIn('platform', self::RECEIPT_MATCH_EXCLUDED_PLATFORMS) ->when($month !== null, static fn ($builder) => $builder->where('report_month', $month)); @@ -2201,7 +2187,7 @@ class Pages extends Controller $filename = '店铺收款匹配订单明细_' . ($month ?? 'all') . '.csv'; - return response()->streamDownload(function () use ($query, $selected, $map, $defaultCompensationAmounts): void { + return response()->streamDownload(function () use ($query, $selected, $map): void { // 单表几十万行:用游标一次查询顺序吐出行,避免 chunk 的 OFFSET 反复重跑 // (29 万行时会退化成几百次排序查询,导出要几分钟)。 @set_time_limit(0); @@ -2214,9 +2200,7 @@ class Pages extends Controller $line = []; foreach ($selected as $key) { $format = $map[$key][2]; - $value = $key === 'default_compensation_amount' - ? ($defaultCompensationAmounts[(string) ($row->row_key ?? '')] ?? '') - : ($row->{$map[$key][0]} ?? ''); + $value = $row->{$map[$key][0]} ?? ''; // 「差额异常备注」列与页面一致:只写差额产生的原因 if ($key === 'remark') { $value = self::receiptDifferenceRemark($row) ?? ''; @@ -2225,7 +2209,10 @@ class Pages extends Controller // 注意:上面取值用了 ?? '',空差值是空字符串而不是 null,这里两种都要认。 if ($key === 'difference' && ($value === null || $value === '')) { $value = round( - (float) ($row->income_amount ?? 0) + (float) ($row->discount_amount ?? 0) + (float) ($row->offset_amount ?? 0), + (float) ($row->income_amount ?? 0) + + (float) ($row->discount_amount ?? 0) + + (float) ($row->offset_amount ?? 0) + + (float) ($row->default_compensation_amount ?? 0), 2 ); } @@ -2557,7 +2544,8 @@ class Pages extends Controller ->where('report_month', $reportMonth) ->whereNotIn('platform', self::RECEIPT_MATCH_EXCLUDED_PLATFORMS) ->selectRaw('platform, store_name, SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' - . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)) as income') + . ' + COALESCE(offset_amount, 0) + COALESCE(default_compensation_amount, 0)' + . ' + COALESCE(refund_amount, 0)) as income') ->groupBy('platform', 'store_name') ->get(); foreach ($incomeRows as $row) { diff --git a/config/business_navigation.php b/config/business_navigation.php index f71eaff0..505396e6 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -252,7 +252,7 @@ $navigation = [ 'directory' => [ 'icon' => 'query_stats', 'status' => '共享镜像', - 'description' => '店铺资料、主体生效区间统一读取团队共享业务镜像;销量取业绩明细的「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按店铺和发生日期逐日汇总后落到主体期间。', + 'description' => '店铺资料、主体生效区间统一读取团队共享业务镜像;销量取业绩明细的「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」合计,按店铺和发生日期逐日汇总后落到主体期间。', 'empty' => '店铺维度报表尚未完成共享数据导入。', 'fields' => [ ['name' => 'erp_store_name', 'label' => 'ERP系统店铺名称'], @@ -822,7 +822,7 @@ $navigation = [ 'directory' => [ 'icon' => 'receipt_long', 'status' => '共享镜像', - 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」按「主单(拆单子单不参与)→ 原单行(merge_so_id 为空)→ 全部行」优先级取数,合单发货(原单与合并行共用同一线上单号、没有主单)时只取本单原单行,避免把合并的另一单算进来;并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(同一套优先级,末档兜底取该单号下最大值)再减去虚拟条码行实付。「优惠金额」「冲减金额」另取总账备注=优惠收入 / 冲减收入的账单金额,不计入收入金额;「退款金额」另取总账备注=退款的账单金额,只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额(两个条件是或的关系),只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额)−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的优惠/冲减会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定:订单实付未匹配(未匹配到订单实付金额,具体原因看「匹配原因」列);订单实付取数为0(种菜)(订单实付=0 且是种菜单)、订单实付取数为0(订单实付=0 但不是种菜单)、订单实付未减联合营销费(仅得物负差额:联合营销费未让利尾数对上才标注)、订单实付取供货价(仅京喜正差额:差额=该单直营服务费(供货模式净收)时标注)、差额=直营补贴款(待开票结算)(仅京喜账单·京喜自营工厂店负差额且无退款(出类拔萃等 10 家已逐店核验,其余工厂店按实付≈商品金额的供货价特征识别):京东直营补贴款待开票结算时标注)、差额=直营补贴款(订单退款已冲回)(同批店负差额但订单已退款:补贴款已随退款冲回)、差额=订单实付包含商家承担优惠和以旧换新国补(仅京东账单·索菲亚客厅/办公/书房/功能、亿家达全屋、朴美家居店负差额、且该单含国补(聚水潭应付−账单收入>0):差额为商家承担优惠(店铺东券/单品折扣等)与以旧换新国补之和,已用营销对账逐单核实)、差额=订单实付包含商家承担优惠(同店负差额但不含国补的单:差额即商家承担优惠)、订单实付未减冲减(订单实付=收入+优惠 → 差额=冲减金额)、订单实付未减优惠(订单实付=收入+冲减 → 差额=优惠金额)、订单实付未减优惠和冲减(订单实付=收入 → 差额=优惠+冲减)(以上三档等式对带口径标识的行让位给口径兜底)、订单实付含已退款部分(抖音负差额:聚水潭退款单/取消子单金额≈差额,或退款合计+账单退款净额≈实付−收入,或整单全退(退款合计≈实付)对上才标注;淘系支付宝/微信负差额:差额=退款合计、或退款合计=实付−收入(收款缺口)、或整单全退(退款合计≈实付)时同样标注(退款取聚水潭已确认记录));以上具体规则均未命中且相关金额在 2 分钱内精确对平时,退款及其与优惠/冲减的组合复用「订单实付含已退款部分」,优惠与冲减组合复用「订单实付未减优惠和冲减」,不新增同义备注;没有可复用的原备注时继续标「差额待核实」。都不满足时,若「卖家实收」或「应付/已付」能对上账单,差额按 0 计并在「匹配原因」列标注口径核对;其余标「差额待核实」。「匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', + 'description' => '覆盖店铺收款账户账单各平台页签(总账备注=订单收入),按「店铺 + 订单号」匹配订单侧商品金额与订单实付金额;一行 = 一个店铺的一个订单,商品金额只挂一次。「商品金额」按「主单(拆单子单不参与)→ 原单行(merge_so_id 为空)→ 全部行」优先级取数,合单发货(原单与合并行共用同一线上单号、没有主单)时只取本单原单行,避免把合并的另一单算进来;并排除虚拟条码(X0000x:信誉订单 / 虚拟订单)明细行;「订单实付金额」取订单级实付(同一套优先级,末档兜底取该单号下最大值)再减去虚拟条码行实付。「优惠金额」「冲减金额」「违约赔付(冲减收入)」分别取对应总账备注的账单净额,不计入收入金额;「退款金额」只单独列示,不计入收入金额、也不参与差额;「种菜金额」是订单号在种菜明细表或聚水潭订单标签明细(是否种菜订单=是)任意一边出现过时,该订单的收入金额,只是收入金额里的一部分,不参与差额;差额=(收入金额+优惠金额+冲减金额+违约赔付(冲减收入))−订单实付金额,匹配状态与差额异常备注同样按订单实付金额判定,未匹配到订单实付金额的行按订单实付金额=0 计入差额,只反映真正对不上的部分。挂不到订单行的抵减项会补成一行(收入/商品留空)。「差额异常备注」只说明差额为什么产生,按顺序自动判定;具体规则均未命中且相关金额在 2 分钱内精确对平时,退款及其与抵减项的组合复用「订单实付含已退款部分」,优惠、冲减与违约赔付组合复用「订单实付未减优惠和冲减」;没有可复用的原备注时继续标「差额待核实」。都不满足时,若「卖家实收」或「应付/已付」能对上账单,差额按 0 计并在「匹配原因」列标注口径核对;其余标「差额待核实」。「匹配原因」单独一列,取值:账单无单号 / 账单无单号(采销收款)/ 单号格式异常 / 订单表无此单 / 1688采销收款(非销售单)/ 订单无实付金额 / 无收款账单(「采销收款」两档专指「支付宝-1688采销专用」账户:该账户收的是采销/供货结算款,不是店铺销售单收款,不按丢单处理)。', 'empty' => '店铺收款匹配订单明细尚未同步,请先执行 store-receipt:order-match:sync。', 'fields' => [ ['name' => 'platform', 'label' => '平台'], diff --git a/config/performance_details.php b/config/performance_details.php index 33600c87..79776e4b 100644 --- a/config/performance_details.php +++ b/config/performance_details.php @@ -9,7 +9,8 @@ return [ * * 两个口径的取数链完全一致(同店同号 → 店铺名不一致 → 同号多条 → 空店铺兜底), * 只是把「订单侧金额」从商品金额换成订单实付金额;差额公式仍是 - * (收入金额 + 优惠金额 + 冲减金额)− 订单侧金额,退款金额单独列示、不参与差额。 + * (收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入))− 订单侧金额, + * 退款金额单独列示、不参与差额。 */ 'difference_basis' => env('PERFORMANCE_DETAILS_DIFFERENCE_BASIS', 'paid'), ]; diff --git a/database/migrations/2026_10_23_180000_add_default_compensation_amount_to_store_receipt_order_match_rows.php b/database/migrations/2026_10_23_180000_add_default_compensation_amount_to_store_receipt_order_match_rows.php new file mode 100644 index 00000000..a9f59915 --- /dev/null +++ b/database/migrations/2026_10_23_180000_add_default_compensation_amount_to_store_receipt_order_match_rows.php @@ -0,0 +1,47 @@ +connection()); + if (! $schema->hasTable('store_receipt_order_match_rows') + || $schema->hasColumn('store_receipt_order_match_rows', 'default_compensation_amount')) { + return; + } + + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->decimal('default_compensation_amount', 20, 4) + ->nullable() + ->after('offset_amount'); + }); + } + + public function down(): void + { + $schema = Schema::connection($this->connection()); + if (! $schema->hasTable('store_receipt_order_match_rows') + || ! $schema->hasColumn('store_receipt_order_match_rows', 'default_compensation_amount')) { + return; + } + + $schema->table('store_receipt_order_match_rows', function (Blueprint $table): void { + $table->dropColumn('default_compensation_amount'); + }); + } +}; diff --git a/modules/EcommerceFinance/Repositories/StoreReportRepository.php b/modules/EcommerceFinance/Repositories/StoreReportRepository.php index e2e4ecdd..fbcea398 100644 --- a/modules/EcommerceFinance/Repositories/StoreReportRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReportRepository.php @@ -17,7 +17,7 @@ use RuntimeException; * * 两列金额口径不同,页面分别展示: * 销量(period_sales / sales_NN):业绩明细(店铺收款匹配订单明细)的 - * 「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」 + * 「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」合计,按「店铺名称 + 发生日期」 * 预汇总到 store_report_paid_sales_rows,再按发生日期落到主体期间; * 收入(rolling_12m_income / income_NN):平台账单日汇总 store_report_daily_rows 的收入, * 供主体维度经营报表按主体汇总使用。 diff --git a/modules/EcommerceFinance/Services/StoreReceiptDefaultCompensationSnapshotBuilder.php b/modules/EcommerceFinance/Services/StoreReceiptDefaultCompensationSnapshotBuilder.php index 76b1eb1a..63be6d9e 100644 --- a/modules/EcommerceFinance/Services/StoreReceiptDefaultCompensationSnapshotBuilder.php +++ b/modules/EcommerceFinance/Services/StoreReceiptDefaultCompensationSnapshotBuilder.php @@ -9,10 +9,10 @@ use PhpOffice\PhpSpreadsheet\IOFactory; use RuntimeException; /** - * 本地预览用:直接从最新科目映射表和账单镜像生成违约赔付金额快照。 + * 人工核对工具:直接从指定科目映射表和账单镜像生成违约赔付金额快照。 * - * 不写共享数据库,只输出 storage/app/local 下的 JSON。正式接入系统前用于核对 - * 「种菜金额」后面的「违约赔付(冲减收入)」列。 + * 不写共享数据库。正式页面已直接读取 store_receipt_order_match_rows 的 + * default_compensation_amount,本工具仅保留用于离线验算。 */ class StoreReceiptDefaultCompensationSnapshotBuilder { diff --git a/modules/EcommerceFinance/Services/StoreReportLogic.php b/modules/EcommerceFinance/Services/StoreReportLogic.php index 4b82ca8f..ac2b3fa0 100644 --- a/modules/EcommerceFinance/Services/StoreReportLogic.php +++ b/modules/EcommerceFinance/Services/StoreReportLogic.php @@ -80,7 +80,7 @@ class StoreReportLogic [ 'code' => 'R9', 'object' => '销量口径', - 'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」逐日汇总;退款、冲减按账单净额(负数)计入,不再使用订单实付/实收金额。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。', + 'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」合计,按「店铺名称 + 发生日期」逐日汇总;退款及各项抵减按账单净额(负数)计入,不再使用订单实付/实收金额。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。', 'source' => 'store_receipt_order_match_rows → store_report_paid_sales_rows', 'priority' => '与业绩明细同口径', ], diff --git a/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php b/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php index c55fb4e3..c73ae33b 100644 --- a/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php @@ -448,7 +448,8 @@ class StoreReportMirrorSynchronizer /** * 店铺维度报表的「销量」:把业绩明细的 - * 「收入金额 + 优惠金额 + 冲减金额 + 退款金额」按「店铺名称 + 发生日期」预汇总。 + * 「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」 + * 按「店铺名称 + 发生日期」预汇总。 * * 业绩明细由 store-receipt:order-match:sync 按「账期 + 平台」整体重写,这里用 * 「行数 + 最近写入时间 + 金额合计」的指纹判断是否变化:没变化就复用当前版本, @@ -502,7 +503,8 @@ class StoreReportMirrorSynchronizer . ' (generation, store_name, occurred_on, paid_amount, order_count, updated_at)' . ' SELECT ?, store_name, occurred_date,' . ' SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' - . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)), COUNT(*), ?' + . ' + COALESCE(offset_amount, 0) + COALESCE(default_compensation_amount, 0)' + . ' + COALESCE(refund_amount, 0)), COUNT(*), ?' . ' FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE . " WHERE store_name IS NOT NULL AND store_name <> '' AND occurred_date IS NOT NULL" . ' GROUP BY store_name, occurred_date', @@ -568,7 +570,8 @@ class StoreReportMirrorSynchronizer . ' (generation, store_name, occurred_on, paid_amount, order_count, updated_at)' . ' SELECT ?, store_name, occurred_date,' . ' SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' - . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)), COUNT(*), ?' + . ' + COALESCE(offset_amount, 0) + COALESCE(default_compensation_amount, 0)' + . ' + COALESCE(refund_amount, 0)), COUNT(*), ?' . ' FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE . " WHERE store_name IS NOT NULL AND store_name <> '' AND occurred_date IN (" . implode(',', array_fill(0, count($dates), '?')) . ') GROUP BY store_name, occurred_date', @@ -593,7 +596,8 @@ class StoreReportMirrorSynchronizer $row = $this->connection->selectOne( 'SELECT COUNT(*) AS rows_total, MAX(imported_at) AS last_imported_at,' . ' COALESCE(SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' - . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)), 0) AS sales_total' + . ' + COALESCE(offset_amount, 0) + COALESCE(default_compensation_amount, 0)' + . ' + COALESCE(refund_amount, 0)), 0) AS sales_total' . ' FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE, ); diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index 20df4686..f7c51096 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -1311,6 +1311,7 @@ 订单实付金额合计{{ number_format((float) ($receiptStats['paid_total'] ?? 0) / 10000, 2) }}万 优惠金额合计{{ number_format((float) ($receiptStats['discount_total'] ?? 0) / 10000, 2) }}万 冲减金额合计{{ number_format((float) ($receiptStats['offset_total'] ?? 0) / 10000, 2) }}万 + 违约赔付(冲减收入)合计{{ number_format((float) ($receiptStats['default_compensation_total'] ?? 0) / 10000, 2) }}万 退款金额合计{{ number_format((float) ($receiptStats['refund_total'] ?? 0) / 10000, 2) }}万 种菜金额合计{{ number_format((float) ($receiptStats['planting_total'] ?? 0) / 10000, 2) }}万 差额合计{{ number_format((float) $receiptStats['difference_total'] / 10000, 2) }}万 diff --git a/tests/Feature/Console/StoreReportMirrorSyncTest.php b/tests/Feature/Console/StoreReportMirrorSyncTest.php index 5a0cb6d7..65696d78 100644 --- a/tests/Feature/Console/StoreReportMirrorSyncTest.php +++ b/tests/Feature/Console/StoreReportMirrorSyncTest.php @@ -228,11 +228,11 @@ class StoreReportMirrorSyncTest extends TestCase $mirror->table('store_receipt_order_match_rows')->insert([ $this->perfRow('k1', '2026-08-02', '店铺A', 100.5), $this->perfRow('k2', '2026-08-02', '店铺A', 9.5), - // 未匹配行没有订单实付、也没有收入,四项金额合计为 0。 + // 未匹配行没有订单实付、收入或抵减金额,销量合计为 0。 $this->perfRow('k3', '2026-08-03', '店铺A', null), $this->perfRow('k4', '2026-08-03', '店铺B', 20), - // 销量=收入 + 优惠 + 冲减 + 退款:100 + 5 + (−3) + (−10) = 92。 - $this->perfRow('k6', '2026-08-04', '店铺C', 100, discount: 5, offset: -3, refund: -10), + // 销量=收入 + 优惠 + 冲减 + 违约赔付 + 退款:100 + 5 + (−3) + (−7) + (−10) = 85。 + $this->perfRow('k6', '2026-08-04', '店铺C', 100, discount: 5, offset: -3, defaultCompensation: -7, refund: -10), ]); $upstream = $this->createMock(StoreReportUpstreamRepository::class); $upstream->method('lifecycleBatch')->willReturn([]); @@ -254,7 +254,7 @@ class StoreReportMirrorSyncTest extends TestCase ], 'business_mirror'); $this->assertDatabaseHas('store_report_paid_sales_rows', [ 'generation' => $generation, 'store_name' => '店铺C', 'occurred_on' => '2026-08-04', - 'paid_amount' => 92, 'order_count' => 1, + 'paid_amount' => 85, 'order_count' => 1, ], 'business_mirror'); $this->assertSame(4, $mirror->table('store_report_paid_sales_rows')->count()); $this->assertDatabaseHas('store_report_import_states', [ @@ -286,6 +286,7 @@ class StoreReportMirrorSyncTest extends TestCase string $importedAt = '2026-10-16 10:00:00', float $discount = 0, float $offset = 0, + float $defaultCompensation = 0, float $refund = 0, ): array { @@ -295,6 +296,7 @@ class StoreReportMirrorSyncTest extends TestCase 'income_amount' => $income, 'discount_amount' => $discount, 'offset_amount' => $offset, + 'default_compensation_amount' => $defaultCompensation, 'refund_amount' => $refund, 'paid_amount' => $income, 'imported_at' => $importedAt, @@ -394,6 +396,7 @@ class StoreReportMirrorSyncTest extends TestCase $table->decimal('income_amount', 20, 4)->nullable(); $table->decimal('discount_amount', 20, 4)->nullable(); $table->decimal('offset_amount', 20, 4)->nullable(); + $table->decimal('default_compensation_amount', 20, 4)->nullable(); $table->decimal('refund_amount', 20, 4)->nullable(); $table->decimal('paid_amount', 20, 4)->nullable(); $table->dateTime('imported_at'); diff --git a/tests/Unit/EcommerceFinance/PerformanceDifferenceRemarkTest.php b/tests/Unit/EcommerceFinance/PerformanceDifferenceRemarkTest.php index 1b2b5509..3f11496d 100644 --- a/tests/Unit/EcommerceFinance/PerformanceDifferenceRemarkTest.php +++ b/tests/Unit/EcommerceFinance/PerformanceDifferenceRemarkTest.php @@ -26,6 +26,7 @@ class PerformanceDifferenceRemarkTest extends TestCase 'income_amount' => 0, 'discount_amount' => 0, 'offset_amount' => 0, + 'default_compensation_amount' => 0, 'refund_amount' => 0, 'product_amount' => 0, 'paid_amount' => 0, @@ -89,6 +90,7 @@ class PerformanceDifferenceRemarkTest extends TestCase $this->assertStringContainsString('refund_amount', $rules['订单实付含已退款部分']); $this->assertStringContainsString('discount_amount', $rules['订单实付未减优惠和冲减']); $this->assertStringContainsString('offset_amount', $rules['订单实付未减优惠和冲减']); + $this->assertStringContainsString('default_compensation_amount', $rules['订单实付未减优惠和冲减']); $this->assertArrayNotHasKey('差额含已退款部分', $rules); $this->assertArrayNotHasKey('差额含优惠/冲减项', $rules); $this->assertArrayNotHasKey('差额=退款金额(金额对平)', $rules); @@ -151,6 +153,19 @@ class PerformanceDifferenceRemarkTest extends TestCase ])); } + public function test_default_compensation_is_counted_once_as_a_separate_adjustment(): void + { + $this->assertSame('订单实付未减优惠和冲减', $this->remark([ + 'platform' => '得物', + 'income_amount' => 100.00, + 'offset_amount' => -14.00, + 'default_compensation_amount' => -25.00, + 'product_amount' => 100.00, + 'paid_amount' => 100.00, + 'difference' => -39.00, + ])); + } + public function test_refund_or_discount_cooccurrence_alone_stays_pending(): void { $this->assertSame('差额待核实', $this->remark([ diff --git a/tests/Unit/EcommerceFinance/StoreReceiptMappingPreviewOverrideTest.php b/tests/Unit/EcommerceFinance/StoreReceiptMappingPreviewOverrideTest.php index 38e4a8a4..eb5d7f06 100644 --- a/tests/Unit/EcommerceFinance/StoreReceiptMappingPreviewOverrideTest.php +++ b/tests/Unit/EcommerceFinance/StoreReceiptMappingPreviewOverrideTest.php @@ -10,13 +10,14 @@ use Tests\TestCase; class StoreReceiptMappingPreviewOverrideTest extends TestCase { - public function test_default_compensation_is_included_in_offset_remarks(): void + public function test_default_compensation_is_separate_from_offset_remarks(): void { $reflection = new ReflectionClass(SyncStoreReceiptOrderMatches::class); - $remarks = $reflection->getConstant('OFFSET_REMARKS'); + $offsetRemarks = $reflection->getConstant('OFFSET_REMARKS'); + $defaultCompensationRemarks = $reflection->getConstant('DEFAULT_COMPENSATION_REMARKS'); - $this->assertContains('冲减收入', $remarks); - $this->assertContains('违约赔付(冲减收入)', $remarks); + $this->assertSame(['冲减收入'], $offsetRemarks); + $this->assertSame(['违约赔付(冲减收入)'], $defaultCompensationRemarks); } public function test_local_mapping_override_file_is_used_when_configured(): void diff --git a/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php b/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php index 02981dac..46472107 100644 --- a/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php +++ b/tests/Unit/EcommerceFinance/StoreReceiptMemoryTest.php @@ -50,6 +50,7 @@ class StoreReceiptMemoryTest extends TestCase $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, 5), $this->bill('退款', '260630-10001', 0, 7), $this->bill('优惠收入', '20002', 4), $this->bill('退款', '20002', 0, 1), @@ -68,6 +69,7 @@ class StoreReceiptMemoryTest extends TestCase $this->assertSame(125.0, $paid['income_amount']); $this->assertSame(-3.0, $paid['discount_amount']); $this->assertSame(-2.0, $paid['offset_amount']); + $this->assertSame(-5.0, $paid['default_compensation_amount']); $this->assertSame(-7.0, $paid['refund_amount']); $this->assertTrue($paid['planting_seen']); $this->assertSame('260630-10001', $paid['online_order_no']); @@ -75,14 +77,16 @@ class StoreReceiptMemoryTest extends TestCase $this->assertSame('天猫_测试店', $paid['store_name']); $this->assertSame(0.0, $result['groups'][$key . '|30003']['income_amount']); $this->assertSame(3, $result['paymentTotal']); - $this->assertSame(12, $result['billCount']); - $this->assertSame(8, $result['moneyRemarkRows']); + $this->assertSame(13, $result['billCount']); + $this->assertSame(9, $result['moneyRemarkRows']); $this->assertSame(125.0, $result['incomeTotal']); $this->assertSame(1.0, $result['discountTotal']); $this->assertSame(-7.0, $result['offsetTotal']); + $this->assertSame(-5.0, $result['defaultCompensationTotal']); $this->assertSame(-10.0, $result['refundTotal']); $this->assertSame(-3.0, $result['attachedDiscount']); $this->assertSame(-2.0, $result['attachedOffset']); + $this->assertSame(-5.0, $result['attachedDefaultCompensation']); $this->assertSame(3, $result['unattachedRows']); $this->assertSame([$key . '|20002', $key . '||冲减收入', $key . '||退款'], array_keys($result['promoOnlyGroups'])); $this->assertSame(4.0, $result['promoOnlyGroups'][$key . '|20002']['discount_amount']); @@ -155,6 +159,36 @@ class StoreReceiptMemoryTest extends TestCase $this->assertSame(2.0, $first['discount_amount']); } + public function test_promo_only_row_carries_default_compensation_separately(): void + { + $row = (new ReflectionMethod(SyncStoreReceiptOrderMatches::class, 'promoOnlyRow'))->invoke( + null, + '2026-08-01', + '得物', + [ + 'order_identity' => '110206461460184356', + 'order_core_no' => '110206461460184356', + 'shop_key' => '得物-墨尚思', + 'bill_item' => '违约赔付', + 'occurred_date' => '2026-08-02', + 'store_name' => '得物-墨尚思', + 'online_order_no' => '110206461460184356', + 'discount_seen' => false, + 'discount_amount' => 0.0, + 'offset_seen' => false, + 'offset_amount' => 0.0, + 'default_compensation_seen' => true, + 'default_compensation_amount' => -25.0, + 'refund_seen' => false, + 'refund_amount' => 0.0, + ], + now(), + ); + + $this->assertNull($row['offset_amount']); + $this->assertSame(-25.0, $row['default_compensation_amount']); + } + public function test_business_remark_and_goods_name_are_forwarded_once_per_bill(): void { $map = $this->createMock(AccountLedgerRemarkMap::class); diff --git a/tests/Unit/EcommerceFinance/StoreReportPaidSalesIncrementalTest.php b/tests/Unit/EcommerceFinance/StoreReportPaidSalesIncrementalTest.php index 2c639789..c554388a 100644 --- a/tests/Unit/EcommerceFinance/StoreReportPaidSalesIncrementalTest.php +++ b/tests/Unit/EcommerceFinance/StoreReportPaidSalesIncrementalTest.php @@ -32,13 +32,13 @@ class StoreReportPaidSalesIncrementalTest extends TestCase $table->date('report_month'); $table->date('occurred_date'); $table->string('store_name'); - foreach (['income_amount', 'discount_amount', 'offset_amount', 'refund_amount'] as $column) $table->decimal($column, 20, 4)->nullable(); + foreach (['income_amount', 'discount_amount', 'offset_amount', 'default_compensation_amount', 'refund_amount'] as $column) $table->decimal($column, 20, 4)->nullable(); }); (require base_path('database/migrations/2026_10_19_171000_track_paid_sales_dirty_dates.php'))->up(); DB::connection('business_mirror')->table('store_receipt_order_match_rows')->insert([ - ['platform' => 'A', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 100, 'discount_amount' => -5, 'offset_amount' => 0, 'refund_amount' => -10], - ['platform' => 'B', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 20, 'discount_amount' => 0, 'offset_amount' => 0, 'refund_amount' => 0], - ['platform' => 'A', 'report_month' => '2026-09-01', 'occurred_date' => '2026-09-30', 'store_name' => '测试店', 'income_amount' => 40, 'discount_amount' => 0, 'offset_amount' => 0, 'refund_amount' => 0], + ['platform' => 'A', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 100, 'discount_amount' => -5, 'offset_amount' => 0, 'default_compensation_amount' => -7, 'refund_amount' => -10], + ['platform' => 'B', 'report_month' => '2026-10-01', 'occurred_date' => '2026-10-01', 'store_name' => '测试店', 'income_amount' => 20, 'discount_amount' => 0, 'offset_amount' => 0, 'default_compensation_amount' => 0, 'refund_amount' => 0], + ['platform' => 'A', 'report_month' => '2026-09-01', 'occurred_date' => '2026-09-30', 'store_name' => '测试店', 'income_amount' => 40, 'discount_amount' => 0, 'offset_amount' => 0, 'default_compensation_amount' => 0, 'refund_amount' => 0], ]); return new StoreReportMirrorSynchronizer(\Mockery::mock(StoreReportUpstreamRepository::class), app('db')); } @@ -53,7 +53,7 @@ class StoreReportPaidSalesIncrementalTest extends TestCase $changes->markPeriod('2026-10', 'A'); $db->table('store_receipt_order_match_rows')->where('id', 1)->update(['income_amount' => 120]); $this->assertTrue($sync->syncPaidSales()['changed']); - $this->assertSame(125.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount')); + $this->assertSame(118.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount')); $this->assertSame($untouchedId, $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-09-30')->value('id')); $db->enableQueryLog(); $this->assertFalse($sync->syncPaidSales()['changed']); @@ -71,12 +71,12 @@ class StoreReportPaidSalesIncrementalTest extends TestCase $changes->markPeriod('2026-10', 'A'); $sync->syncPaidSales(); $this->assertSame(20.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount')); - $this->assertSame(85.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-03')->value('paid_amount')); + $this->assertSame(78.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-03')->value('paid_amount')); $changes->markPeriod('2026-10', 'B'); $db->table('store_receipt_order_match_rows')->where('platform', 'B')->delete(); $sync->syncPaidSales(); $this->assertSame(0, $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->count()); - $this->assertSame(125.0, (float) $db->table('store_report_paid_sales_rows')->sum('paid_amount')); + $this->assertSame(118.0, (float) $db->table('store_report_paid_sales_rows')->sum('paid_amount')); } public function test_a_new_revision_is_not_lost_when_an_older_snapshot_is_acknowledged(): void @@ -119,7 +119,7 @@ class StoreReportPaidSalesIncrementalTest extends TestCase if (str_starts_with($query->sql, 'INSERT INTO store_report_paid_sales_rows')) throw new \RuntimeException('test_aggregate_failure'); }); $this->assertSame('failed', $sync->syncPaidSales()['status']); - $this->assertSame(105.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount')); + $this->assertSame(98.0, (float) $db->table('store_report_paid_sales_rows')->where('occurred_on', '2026-10-01')->value('paid_amount')); $this->assertCount(1, app(StoreReportPaidSalesChanges::class)->pending()); } }