fix: 拆分违约赔付与普通冲减金额 #481
@ -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
|
||||
{
|
||||
|
||||
@ -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<string,mixed> $source
|
||||
* @param array<string,array<string,mixed>> $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<string,int> $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,
|
||||
|
||||
@ -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<string,float>
|
||||
*/
|
||||
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) {
|
||||
|
||||
@ -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' => '平台'],
|
||||
|
||||
@ -9,7 +9,8 @@ return [
|
||||
*
|
||||
* 两个口径的取数链完全一致(同店同号 → 店铺名不一致 → 同号多条 → 空店铺兜底),
|
||||
* 只是把「订单侧金额」从商品金额换成订单实付金额;差额公式仍是
|
||||
* (收入金额 + 优惠金额 + 冲减金额)− 订单侧金额,退款金额单独列示、不参与差额。
|
||||
* (收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入))− 订单侧金额,
|
||||
* 退款金额单独列示、不参与差额。
|
||||
*/
|
||||
'difference_basis' => env('PERFORMANCE_DETAILS_DIFFERENCE_BASIS', 'paid'),
|
||||
];
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 业绩明细把「违约赔付(冲减收入)」从普通「冲减收入」中拆开。
|
||||
*
|
||||
* 金额仍按账单行「收入 - 支出」保存,负数表示净扣减;差额和收入汇总会
|
||||
* 分别计入本列,避免只保留在 offset_amount 里造成口径混淆。
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
private function connection(): string
|
||||
{
|
||||
return (string) config('upstream_mirrors.connection', 'business_mirror');
|
||||
}
|
||||
|
||||
public function up(): 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->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -17,7 +17,7 @@ use RuntimeException;
|
||||
*
|
||||
* 两列金额口径不同,页面分别展示:
|
||||
* 销量(period_sales / sales_NN):业绩明细(店铺收款匹配订单明细)的
|
||||
* 「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」
|
||||
* 「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」合计,按「店铺名称 + 发生日期」
|
||||
* 预汇总到 store_report_paid_sales_rows,再按发生日期落到主体期间;
|
||||
* 收入(rolling_12m_income / income_NN):平台账单日汇总 store_report_daily_rows 的收入,
|
||||
* 供主体维度经营报表按主体汇总使用。
|
||||
|
||||
@ -9,10 +9,10 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 本地预览用:直接从最新科目映射表和账单镜像生成违约赔付金额快照。
|
||||
* 人工核对工具:直接从指定科目映射表和账单镜像生成违约赔付金额快照。
|
||||
*
|
||||
* 不写共享数据库,只输出 storage/app/local 下的 JSON。正式接入系统前用于核对
|
||||
* 「种菜金额」后面的「违约赔付(冲减收入)」列。
|
||||
* 不写共享数据库。正式页面已直接读取 store_receipt_order_match_rows 的
|
||||
* default_compensation_amount,本工具仅保留用于离线验算。
|
||||
*/
|
||||
class StoreReceiptDefaultCompensationSnapshotBuilder
|
||||
{
|
||||
|
||||
@ -80,7 +80,7 @@ class StoreReportLogic
|
||||
[
|
||||
'code' => 'R9',
|
||||
'object' => '销量口径',
|
||||
'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」逐日汇总;退款、冲减按账单净额(负数)计入,不再使用订单实付/实收金额。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。',
|
||||
'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「收入金额 + 优惠金额 + 冲减金额 + 违约赔付(冲减收入) + 退款金额」合计,按「店铺名称 + 发生日期」逐日汇总;退款及各项抵减按账单净额(负数)计入,不再使用订单实付/实收金额。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。',
|
||||
'source' => 'store_receipt_order_match_rows → store_report_paid_sales_rows',
|
||||
'priority' => '与业绩明细同口径',
|
||||
],
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -1311,6 +1311,7 @@
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>订单实付金额合计</em><strong>{{ number_format((float) ($receiptStats['paid_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>优惠金额合计</em><strong>{{ number_format((float) ($receiptStats['discount_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>冲减金额合计</em><strong>{{ number_format((float) ($receiptStats['offset_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>违约赔付(冲减收入)合计</em><strong>{{ number_format((float) ($receiptStats['default_compensation_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>退款金额合计</em><strong>{{ number_format((float) ($receiptStats['refund_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount"><em>种菜金额合计</em><strong>{{ number_format((float) ($receiptStats['planting_total'] ?? 0) / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
<span class="receipt-match-summary__item receipt-match-summary__item--amount receipt-match-summary__item--lead"><em>差额合计</em><strong>{{ number_format((float) $receiptStats['difference_total'] / 10000, 2) }}<span class="receipt-match-summary__value-unit">万</span></strong></span>
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user