CaiWuXiTong/modules/EcommerceFinance/Repositories/CourierSurchargeSettlementMirrorRepository.php

1611 lines
60 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Repositories;
use Carbon\CarbonInterface;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
use RuntimeException;
use Throwable;
/**
* 「快递应付 → 附加费报价维护 → 附加费结算」的只读仓库。
*
* 结算行由 `courier-surcharge:pull-overlength` 按启用的附加费报价规则
* (第一期只处理“超长费 + 按尺寸/最长边”)从销售出库单拉取后写入
* courier_surcharge_settlement_rows;页面只读该表,不在请求中回源上游。
*/
class CourierSurchargeSettlementMirrorRepository
{
public const TABLE = 'courier_surcharge_settlement_rows';
/** 页面附加费类型下拉使用的固定类型。 */
public const SURCHARGE_TYPES = [
'退回费', '拦截费', '改地址', '上楼费', '送货费', '超区费', '安装费', '保费', '进仓费', '超长费',
];
/** 超出多久没有重新拉取就标记为“稍有延迟”。 */
private const STALE_AFTER_MINUTES = 1440;
/** 查件工单里「查件类型/原因」级联字段。 */
private const CHECK_TYPE_FIELD = 'SELECT_CASCADEField1413856620250813172526';
/** 导入核验时最多回查多少张扣单工单,避免大表扫描过重。 */
private const IMPORT_RETURN_CONTEXT_LIMIT = 100;
/** 导入核验时最多按单号回查多少张销售出库单(超长费)。 */
private const IMPORT_OVERLENGTH_CONTEXT_LIMIT = 200;
private readonly ConnectionInterface $connection;
public function __construct(DatabaseManager $database)
{
$this->connection = $database->connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
public function available(): bool
{
try {
return Schema::connection($this->connection->getName())->hasTable(self::TABLE);
} catch (Throwable) {
return false;
}
}
public function ready(): bool
{
return $this->available() && $this->connection->table(self::TABLE)->exists();
}
/** @return array<string,mixed> */
public function state(): array
{
if (! $this->available()) {
return [
'code' => 'pending',
'status' => 'pending',
'last_succeeded_at' => null,
'mirror_row_count' => 0,
];
}
try {
$lastPulledAt = $this->connection->table(self::TABLE)->max('pulled_at');
$rowCount = (int) $this->connection->table(self::TABLE)->count();
} catch (Throwable) {
return [
'code' => 'pending',
'status' => 'pending',
'last_succeeded_at' => null,
'mirror_row_count' => 0,
];
}
return [
'code' => $lastPulledAt === null ? 'pending' : 'fresh',
'status' => $lastPulledAt === null ? 'pending' : 'succeeded',
'last_succeeded_at' => $lastPulledAt,
'mirror_row_count' => $rowCount,
];
}
/** @param array<string,mixed>|null $state @return array{code:string,lag_minutes:?int} */
public function syncStatus(?array $state = null, ?CarbonInterface $at = null): array
{
$state ??= $this->state();
$lastSucceeded = $this->timestamp($state['last_succeeded_at'] ?? null);
if ($lastSucceeded === null) {
return ['code' => 'pending', 'lag_minutes' => null];
}
$lagMinutes = (int) ceil(max(0, ($at ?? now())->getTimestamp() - $lastSucceeded) / 60);
return [
'code' => $lagMinutes > self::STALE_AFTER_MINUTES ? 'stale' : 'fresh',
'lag_minutes' => $lagMinutes,
];
}
/**
* @param array<string,string> $filters
* @return array{data:list<array<string,mixed>>,total:int,has_more:bool,summary:array<string,mixed>,sync:array<string,mixed>,state:array<string,mixed>}
*/
public function paginate(array $filters, int $page, int $limit, mixed $quotes = null): array
{
if (! $this->ready()) {
throw new RuntimeException('surcharge_settlement_rows_not_ready');
}
$page = max($page, 1);
$limit = min(max($limit, 1), 100);
$query = $this->filteredQuery($filters);
$total = (int) (clone $query)->count();
$rows = $query
->orderByDesc('shipped_at')
->orderByDesc('id')
->forPage($page, $limit)
->get();
$data = [];
$amount = 0.0;
foreach ($rows as $row) {
$mapped = $this->mapRow($row);
$amount += (float) ($mapped['settlement_amount'] ?? 0);
$data[] = $mapped;
}
$state = $this->state();
return [
'data' => $data,
'total' => $total,
'has_more' => ($page * $limit) < $total,
'summary' => ['count' => $total, 'settlement_amount' => round($amount, 2)],
'sync' => ['code' => $state['code'], 'last_succeeded_at' => $state['last_succeeded_at']],
'state' => $state,
];
}
/** @param array<string,string> $filters */
private function filteredQuery(array $filters): Builder
{
$query = $this->connection->table(self::TABLE);
foreach ([
'courier_name' => 'courier_name',
'waybill' => 'waybill',
'platform_product_id' => 'platform_product_id',
'product_code' => 'product_code',
'shop_name' => 'shop_name',
'online_order_no' => 'online_order_no',
'inspection_type' => 'inspection_type',
'inspection_reason' => 'inspection_reason',
] as $filter => $column) {
$value = trim((string) ($filters[$filter] ?? ''));
if ($value !== '') {
$query->whereRaw($column . " LIKE ? ESCAPE '!'", [$this->contains($value)]);
}
}
if (trim((string) ($filters['surcharge_type'] ?? '')) !== '') {
$query->where('surcharge_type', trim((string) $filters['surcharge_type']));
}
if (trim((string) ($filters['status'] ?? '')) !== '') {
$query->where('settlement_status', trim((string) $filters['status']));
}
if (trim((string) ($filters['created_from'] ?? '')) !== '') {
$query->where('shipped_at', '>=', $filters['created_from'] . ' 00:00:00');
}
if (trim((string) ($filters['created_to'] ?? '')) !== '') {
$query->where('shipped_at', '<', Carbon::parse($filters['created_to'])->addDay()->toDateTimeString());
}
return $query;
}
/**
* 导出用流式游标:按筛选条件顺序读取结算行,默认最多 5 万行。
*
* @param array<string,string> $filters
* @return \Generator<int,object>
*/
public function exportRows(array $filters, int $maxRows = 50000): \Generator
{
if (! $this->ready()) {
throw new RuntimeException('surcharge_settlement_rows_not_ready');
}
$query = $this->filteredQuery($filters)
->orderByDesc('shipped_at')
->orderByDesc('id')
->limit(min(max($maxRows, 1), 50000));
foreach ($query->cursor() as $row) {
yield $row;
}
}
/**
* 导入核验:按「单号 + 附加费类型」匹配结算行,给出正常结/不结结论与原因。
*
* @param list<array{row_number:int,waybill:string,surcharge_type:string}> $pairs
* @return list<array<string,mixed>>
*/
public function decideImportRows(array $pairs): array
{
if (! $this->ready()) {
throw new RuntimeException('surcharge_settlement_rows_not_ready');
}
$waybills = [];
foreach ($pairs as $pair) {
$waybill = trim((string) ($pair['waybill'] ?? ''));
if ($waybill !== '') {
$waybills[$waybill] = true;
}
}
$rowsByWaybill = [];
foreach (array_chunk(array_keys($waybills), 300) as $chunk) {
$rows = $this->connection->table(self::TABLE)
->whereIn('waybill', $chunk)
->orderByDesc('settlement_amount')
->orderByDesc('id')
->get();
foreach ($rows as $row) {
$waybill = trim((string) $row->waybill);
if ($waybill !== '') {
$rowsByWaybill[$waybill][] = $row;
}
}
}
$normalized = [];
foreach ($pairs as $pair) {
$normalized[] = [
'row_number' => (int) ($pair['row_number'] ?? 0),
'waybill' => trim((string) ($pair['waybill'] ?? '')),
'surcharge_type' => $this->normalizeImportType((string) ($pair['surcharge_type'] ?? '')),
'courier_name' => trim((string) ($pair['courier_name'] ?? '')),
];
}
$unmatchedDeductWaybills = [];
foreach ($normalized as $pair) {
if ($pair['waybill'] === '' || ! in_array($pair['surcharge_type'], ['退回费', '拦截费'], true)) {
continue;
}
if ($this->matchImportRow($rowsByWaybill[$pair['waybill']] ?? [], $pair['surcharge_type'], $pair['courier_name']) === null) {
$unmatchedDeductWaybills[$pair['waybill']] = true;
}
}
$contextComplete = count($unmatchedDeductWaybills) <= self::IMPORT_RETURN_CONTEXT_LIMIT;
$returnContext = $this->deductOrderContext(array_keys($unmatchedDeductWaybills));
$ruleModes = $this->activeRuleModes();
$quoteRules = $this->importQuoteRules();
// 超长费没有结算行时,按单号回销售出库单查快递/尺寸并按启用的报价核算。
$unmatchedOverlengthWaybills = [];
foreach ($normalized as $pair) {
if ($pair['waybill'] === '' || $pair['surcharge_type'] !== '超长费') {
continue;
}
if ($this->matchImportRow($rowsByWaybill[$pair['waybill']] ?? [], $pair['surcharge_type'], $pair['courier_name']) === null) {
$unmatchedOverlengthWaybills[$pair['waybill']] = true;
}
}
$overlengthContext = $this->importOverlengthContext(array_keys($unmatchedOverlengthWaybills));
$fallbackRows = [];
foreach ($rowsByWaybill as $waybill => $rows) {
if ($rows !== []) {
$fallbackRows[$waybill] = $rows[0];
}
}
$contextSkuIds = [];
foreach ($returnContext as $context) {
foreach ($context['items'] ?? [] as $item) {
$skuId = trim((string) ($item['sku_id'] ?? ''));
if ($skuId !== '') {
$contextSkuIds[$skuId] = true;
}
}
}
$contextWeights = $this->importWeights(array_keys($contextSkuIds));
$contextReturns = $this->importReturns($returnContext);
$results = [];
foreach ($normalized as $pair) {
$matched = null;
$mismatchedCourier = '';
if ($pair['waybill'] !== '') {
$candidates = $rowsByWaybill[$pair['waybill']] ?? [];
$matched = $this->matchImportRow($candidates, $pair['surcharge_type'], $pair['courier_name']);
if ($matched === null && $pair['courier_name'] !== '') {
$other = $this->matchImportRow($candidates, $pair['surcharge_type']);
if ($other !== null) {
$mismatchedCourier = trim((string) $other->courier_name);
}
}
}
$results[] = $matched !== null
? $this->importMatchedRow($pair, $matched)
: ($this->importQuoteResult(
$pair,
$fallbackRows[$pair['waybill']] ?? null,
$returnContext,
$ruleModes,
$quoteRules,
$overlengthContext,
$contextWeights,
$contextReturns,
) ?? $this->importMissingRow(
$pair,
$returnContext,
$ruleModes,
$contextComplete,
$mismatchedCourier,
$fallbackRows[$pair['waybill']] ?? null,
$contextWeights,
$contextReturns,
));
}
return $results;
}
private function normalizeImportType(string $type): string
{
$type = trim($type);
return [
'退回件' => '退回费',
'退货费' => '退回费',
'退货件' => '退回费',
'超长' => '超长费',
'改地址费' => '改地址',
][$type] ?? $type;
}
/**
* @param iterable<object> $rows
*/
private function matchImportRow(iterable $rows, string $type, string $courier = ''): ?object
{
$fallback = null;
foreach ($rows as $row) {
if (trim((string) $row->surcharge_type) !== $type) {
continue;
}
if ($courier !== '' && trim((string) $row->courier_name) !== $courier) {
continue;
}
$fallback ??= $row;
if ((float) ($row->settlement_amount ?? 0) > 0) {
return $row;
}
}
return $fallback;
}
/**
* @param array{row_number:int,waybill:string,surcharge_type:string} $pair
* @return array<string,mixed>
*/
private function importMatchedRow(array $pair, object $row): array
{
$amount = (float) ($row->settlement_amount ?? 0);
$status = trim((string) ($row->settlement_status ?? ''));
$evidence = trim((string) ($row->evidence ?? ''));
$reason = '';
if ($amount > 0) {
$decision = '正常结';
} else {
$decision = '不结';
if ($status === '未匹配报价') {
$reason = '未匹配到启用的报价规则';
} elseif ($status === '待核价') {
$reason = '待核价:商品资料缺少重量或规则金额无法计算';
} elseif (str_contains($evidence, '无进仓') || str_contains($evidence, '无退货')) {
$reason = '售后退货退款无进仓时间/实退数量';
} else {
$reason = '结算金额为 0';
}
}
$mapped = $this->mapRow($row);
$mapped['settlement_status'] = $status;
$mapped['evidence'] = $evidence;
return array_merge([
'row_number' => $pair['row_number'],
'waybill' => $pair['waybill'],
'surcharge_type' => $pair['surcharge_type'],
'decision' => $decision,
'reason' => $reason,
], $mapped);
}
/**
* @param array{row_number:int,waybill:string,surcharge_type:string,courier_name:string} $pair
* @param array<string,array<string,mixed>> $returnContext
* @param array<string,array<string,list<string>>> $ruleModes
* @param array<string,float> $contextWeights
* @param array<int,list<array<string,mixed>>> $contextReturns
* @return array<string,mixed>
*/
private function importMissingRow(
array $pair,
array $returnContext,
array $ruleModes,
bool $contextComplete,
string $mismatchedCourier,
?object $fallbackRow,
array $contextWeights,
array $contextReturns,
): array {
$context = $returnContext[$pair['waybill']] ?? null;
$courier = $pair['courier_name'];
if ($courier === '') {
$courier = trim((string) ($fallbackRow->courier_name ?? ($context['courier'] ?? '')));
}
$platformProductId = null;
$productCode = null;
$shopName = null;
$onlineOrderNo = null;
$inspectionType = null;
$inspectionReason = null;
$returnWaybillNo = null;
$settlementWeight = null;
$shippedAt = null;
$evidence = null;
if ($fallbackRow !== null) {
// 同一运单已有其他类型的结算行时,直接复用其来源字段。
$platformProductId = $fallbackRow->platform_product_id;
$productCode = $fallbackRow->product_code;
$shopName = $fallbackRow->shop_name;
$onlineOrderNo = $fallbackRow->online_order_no;
$inspectionType = $fallbackRow->inspection_type;
$inspectionReason = $fallbackRow->inspection_reason;
$returnWaybillNo = $fallbackRow->return_waybill_no;
$settlementWeight = $fallbackRow->settlement_weight;
$shippedAt = $fallbackRow->shipped_at;
$evidence = '未生成「' . $pair['surcharge_type'] . '」结算行;同运单已有「' . $fallbackRow->surcharge_type . '」结算记录';
} elseif ($context !== null) {
// 没有任何结算行时,从查件扣单工单补齐快递、商品、店铺、线上下单等信息。
$primary = $this->primaryImportItem($context['items'] ?? []);
$platformProductId = $primary['platform_product_id'];
$productCode = $primary['sku_id'] !== '' ? $primary['sku_id'] : $primary['i_id'];
$shopName = trim((string) ($context['shop_name'] ?? ''));
$onlineOrderNo = trim((string) ($context['online_order_no'] ?? ''));
$inspectionType = trim((string) ($context['inspection_type'] ?? '')) ?: null;
$inspectionReason = trim((string) ($context['inspection_reason'] ?? ''));
$orderReturns = $contextReturns[(int) ($context['o_id'] ?? 0)] ?? [];
$returnWaybills = array_values(array_unique(array_filter(array_column($orderReturns, 'l_id'))));
$returnWaybillNo = $returnWaybills === [] ? null : implode(',', $returnWaybills);
$settlementWeight = $this->importWeightSum($context['items'] ?? [], $contextWeights);
$sendTime = trim((string) ($context['send_time'] ?? ''));
$shippedAt = $sendTime !== '' ? $sendTime : null;
$evidence = ($inspectionType ?? '查件') . ';未生成「' . $pair['surcharge_type'] . '」结算行';
$platformProductId = $platformProductId !== '' ? $platformProductId : null;
$productCode = $productCode !== '' ? $productCode : null;
$shopName = $shopName !== '' ? $shopName : null;
$onlineOrderNo = $onlineOrderNo !== '' ? $onlineOrderNo : null;
$inspectionReason = $inspectionReason !== '' ? $inspectionReason : null;
}
return [
'row_number' => $pair['row_number'],
'waybill' => $pair['waybill'],
'surcharge_type' => $pair['surcharge_type'],
'decision' => '不结',
'reason' => $this->missingReason($pair, $returnContext, $ruleModes, $contextComplete, $mismatchedCourier),
'courier_name' => $courier !== '' ? $courier : null,
'shipped_at' => $shippedAt,
'platform_product_id' => $platformProductId,
'product_code' => $productCode,
'shop_name' => $shopName,
'online_order_no' => $onlineOrderNo,
'length' => null,
'width' => null,
'height' => null,
'volume' => null,
'inspection_type' => $inspectionType,
'inspection_reason' => $inspectionReason,
'return_waybill_no' => $returnWaybillNo !== null && $returnWaybillNo !== ''
? mb_substr((string) $returnWaybillNo, 0, 191)
: null,
'settlement_weight' => $settlementWeight !== null && (float) $settlementWeight > 0
? round((float) $settlementWeight, 2)
: null,
'settlement_amount' => null,
'settlement_status' => null,
'evidence' => $evidence,
];
}
/**
* 导入核验的「按报价核算」:结算表里没有该运单+类型的结算行时,
* 用单号回销售出库单(超长费)或查件扣单(退回费/拦截费)取上下文,
* 再按启用的报价规则计算应结金额。返回 null 表示该单号取不到上下文,
* 交给 importMissingRow 输出兜底的不结原因。
*
* @param array{row_number:int,waybill:string,surcharge_type:string,courier_name:string} $pair
* @param array<string,array<string,mixed>> $returnContext
* @param array<string,array<string,list<string>>> $ruleModes
* @param array{overlength:list<array<string,mixed>>,return:list<array<string,mixed>>,intercept:list<array<string,mixed>>} $quoteRules
* @param array<string,array<string,mixed>> $overlengthContext
* @param array<string,float> $contextWeights
* @param array<int,list<array<string,mixed>>> $contextReturns
* @return array<string,mixed>|null
*/
private function importQuoteResult(
array $pair,
?object $fallbackRow,
array $returnContext,
array $ruleModes,
array $quoteRules,
array $overlengthContext,
array $contextWeights,
array $contextReturns,
): ?array {
$type = $pair['surcharge_type'];
if ($type === '超长费') {
$context = $overlengthContext[$pair['waybill']] ?? null;
return $context === null
? null
: $this->importOverlengthQuoteResult($pair, $context, $quoteRules['overlength'] ?? []);
}
if (! in_array($type, ['退回费', '拦截费'], true)) {
return null;
}
$context = $returnContext[$pair['waybill']] ?? null;
return $context === null
? null
: $this->importDeductQuoteResult($pair, $context, $fallbackRow, $ruleModes, $quoteRules, $contextWeights, $contextReturns);
}
/**
* 按单号回销售出库单(JianSun)取快递、明细、尺寸与平台商品ID。
*
* @param list<string> $waybills
* @return array<string,array<string,mixed>>
*/
private function importOverlengthContext(array $waybills): array
{
if ($waybills === [] || count($waybills) > self::IMPORT_OVERLENGTH_CONTEXT_LIMIT) {
return [];
}
try {
$upstream = app(UpstreamReadOnlyQueryFactory::class)->forSource('jiansun');
} catch (Throwable) {
return [];
}
$orders = [];
$orderByWaybill = [];
foreach (array_chunk($waybills, 100) as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
try {
$rows = $upstream->select(
'SELECT Id, io_id, o_id, l_id, so_id, shop_name, '
. "CONVERT(varchar(19), io_date, 120) AS io_date, logistics_company "
. 'FROM dbo.jst_salesOutboundInfo WHERE l_id IN (' . $placeholders . ') ORDER BY Id DESC',
$chunk,
);
} catch (Throwable) {
continue;
}
foreach ($rows as $row) {
$row = (array) $row;
$waybill = trim((string) ($row['l_id'] ?? ''));
if ($waybill === '' || isset($orderByWaybill[$waybill])) {
continue;
}
$orderByWaybill[$waybill] = $row;
$orders[] = $row;
}
}
if ($orders === []) {
return [];
}
$ioIds = array_values(array_unique(array_filter(array_map(
static fn (array $order): int => (int) ($order['io_id'] ?? 0),
$orders,
))));
$items = [];
$skuIds = [];
foreach (array_chunk($ioIds, 200) as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
try {
$rows = $upstream->select(
'SELECT io_id, sku_id, i_id, qty FROM dbo.jst_salesOutboundItem WHERE io_id IN (' . $placeholders . ')',
$chunk,
);
} catch (Throwable) {
continue;
}
foreach ($rows as $row) {
$row = (array) $row;
$ioId = (int) ($row['io_id'] ?? 0);
$skuId = trim((string) ($row['sku_id'] ?? ''));
if ($ioId <= 0) {
continue;
}
$items[$ioId][] = $row;
if ($skuId !== '') {
$skuIds[$skuId] = true;
}
}
}
$dimensions = $this->importDimensions(array_keys($skuIds));
$platform = [];
foreach (array_chunk($ioIds, 200) as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
try {
$rows = $upstream->select(
'SELECT io_id, sku_id, shop_i_id FROM dbo.Jst_SendDataDetail_View WHERE io_id IN (' . $placeholders . ')'
. ' AND LEN(shop_i_id) > 0',
$chunk,
);
} catch (Throwable) {
continue;
}
foreach ($rows as $row) {
$row = (array) $row;
$ioId = (int) ($row['io_id'] ?? 0);
$shopIId = trim((string) ($row['shop_i_id'] ?? ''));
$skuId = trim((string) ($row['sku_id'] ?? ''));
if ($ioId <= 0 || $shopIId === '') {
continue;
}
if ($skuId !== '') {
$platform[$ioId . '|' . $skuId] = $shopIId;
}
$platform[$ioId . '|'] ??= $shopIId;
}
}
$contexts = [];
foreach ($orderByWaybill as $waybill => $order) {
$ioId = (int) ($order['io_id'] ?? 0);
$contexts[$waybill] = [
'order' => $order,
'items' => $items[$ioId] ?? [],
'dimensions' => $dimensions,
'platform' => $platform,
];
}
return $contexts;
}
/**
* @param array<string,mixed> $context
* @param list<array{id:int,courier:string,threshold:float,amount:float}> $rules
* @return array<string,mixed>
*/
private function importOverlengthQuoteResult(array $pair, array $context, array $rules): array
{
$order = $context['order'];
$actualCourier = trim((string) ($order['logistics_company'] ?? ''));
$importedCourier = $pair['courier_name'];
$courier = $importedCourier !== '' ? $importedCourier : $actualCourier;
$ioId = (int) ($order['io_id'] ?? 0);
$best = null;
foreach ($context['items'] as $item) {
$skuId = trim((string) ($item['sku_id'] ?? ''));
$dimension = $context['dimensions'][$skuId] ?? null;
if ($dimension === null) {
continue;
}
$maxSide = max($dimension['l'], $dimension['w'], $dimension['h']);
if ($best === null || $maxSide > $best['max_side']) {
$best = [
'max_side' => $maxSide,
'sku_id' => $skuId,
'i_id' => trim((string) ($item['i_id'] ?? '')),
'length' => $dimension['l'],
'width' => $dimension['w'],
'height' => $dimension['h'],
'platform_product_id' => $context['platform'][$ioId . '|' . $skuId]
?? ($context['platform'][$ioId . '|'] ?? null),
];
}
}
$fields = [
'courier_name' => $courier !== '' ? $courier : null,
'shipped_at' => trim((string) ($order['io_date'] ?? '')) ?: null,
'platform_product_id' => $best['platform_product_id'] ?? null,
'product_code' => $best === null
? null
: ($best['sku_id'] !== '' ? $best['sku_id'] : ($best['i_id'] !== '' ? $best['i_id'] : null)),
'shop_name' => trim((string) ($order['shop_name'] ?? '')) ?: null,
'online_order_no' => trim((string) ($order['so_id'] ?? '')) ?: null,
'length' => $best['length'] ?? null,
'width' => $best['width'] ?? null,
'height' => $best['height'] ?? null,
'volume' => $best === null ? null : round($best['length'] * $best['width'] * $best['height'], 2),
'inspection_type' => null,
'inspection_reason' => null,
'return_waybill_no' => null,
'settlement_weight' => null,
];
if ($importedCourier !== '' && $actualCourier !== '' && $importedCourier !== $actualCourier) {
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:该运单实际快递为「' . $actualCourier . '」,与导入的「' . $importedCourier . '」不一致', null);
}
$rule = null;
foreach ($rules as $candidate) {
if ($candidate['courier'] === $actualCourier) {
$rule = $candidate;
break;
}
}
if ($rule === null) {
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:快递「' . ($actualCourier !== '' ? $actualCourier : '未知') . '」没有启用的「超长费」报价规则', null);
}
if ($best === null) {
return $this->importQuoteRow($pair, $fields, null, '待核价',
'待核价:商品资料缺少长宽高,暂无法核算超长费', '销售出库单已匹配,但条形码在商品资料中没有长宽高');
}
$evidence = mb_substr(sprintf(
'规则 #%d;条形码 %s 尺寸 %s×%s×%s,最长边 %scm %s %scm',
$rule['id'],
$best['sku_id'] !== '' ? $best['sku_id'] : '(无)',
$this->trimNumber($best['length']),
$this->trimNumber($best['width']),
$this->trimNumber($best['height']),
$this->trimNumber($best['max_side']),
$best['max_side'] > $rule['threshold'] ? '>' : '≤',
$this->trimNumber($rule['threshold']),
), 0, 500);
if ($best['max_side'] <= $rule['threshold']) {
return $this->importQuoteRow($pair, $fields, 0.0, '已计算',
'不结:最长边 ' . $this->trimNumber($best['max_side']) . 'cm 未超过报价阈值 ' . $this->trimNumber($rule['threshold']) . 'cm',
$evidence);
}
return $this->importQuoteRow($pair, $fields, (float) $rule['amount'], '已计算', '', $evidence);
}
/**
* @param array<string,mixed> $context
* @param array<string,array<string,list<string>>> $ruleModes
* @param array{overlength:list<array<string,mixed>>,return:list<array<string,mixed>>,intercept:list<array<string,mixed>>} $quoteRules
* @param array<string,float> $contextWeights
* @param array<int,list<array<string,mixed>>> $contextReturns
* @return array<string,mixed>
*/
private function importDeductQuoteResult(
array $pair,
array $context,
?object $fallbackRow,
array $ruleModes,
array $quoteRules,
array $contextWeights,
array $contextReturns,
): array {
$type = $pair['surcharge_type'];
$importedCourier = $pair['courier_name'];
$contextCourier = trim((string) ($context['courier'] ?? ''));
$fallbackCourier = trim((string) ($fallbackRow->courier_name ?? ''));
$courier = $importedCourier !== '' ? $importedCourier : ($contextCourier !== '' ? $contextCourier : $fallbackCourier);
$inspectionType = trim((string) ($context['inspection_type'] ?? ''));
$items = $context['items'] ?? [];
$primary = $this->primaryImportItem($items);
$orderWeight = $this->importWeightSum($items, $contextWeights) ?? 0.0;
$oId = (int) ($context['o_id'] ?? 0);
$returnWaybills = [];
$hasReturn = false;
$returnedWeight = 0.0;
$hasReturnWeight = false;
foreach ($contextReturns[$oId] ?? [] as $return) {
$rQty = (float) ($return['r_qty'] ?? 0);
$receiveDate = trim((string) ($return['receive_date'] ?? ''));
if ($rQty <= 0 || $receiveDate === '') {
continue;
}
$hasReturn = true;
$returnWaybill = trim((string) ($return['l_id'] ?? ''));
if ($returnWaybill !== '') {
$returnWaybills[$returnWaybill] = true;
}
$skuId = strtoupper(trim((string) ($return['sku_id'] ?? '')));
$weight = $skuId !== '' ? ($contextWeights[$skuId] ?? null) : null;
if ($weight !== null) {
$returnedWeight += $rQty * (float) $weight;
$hasReturnWeight = true;
}
}
$settlementWeight = $hasReturn
? ($hasReturnWeight && $returnedWeight > 0 ? $returnedWeight : $orderWeight)
: $orderWeight;
$fields = [
'courier_name' => $courier !== '' ? $courier : null,
'shipped_at' => trim((string) ($context['send_time'] ?? '')) ?: null,
'platform_product_id' => $primary['platform_product_id'] !== '' ? $primary['platform_product_id'] : null,
'product_code' => $primary['sku_id'] !== '' ? $primary['sku_id'] : ($primary['i_id'] !== '' ? $primary['i_id'] : null),
'shop_name' => trim((string) ($context['shop_name'] ?? '')) ?: null,
'online_order_no' => trim((string) ($context['online_order_no'] ?? '')) ?: null,
'length' => null,
'width' => null,
'height' => null,
'volume' => null,
'inspection_type' => $inspectionType !== '' ? $inspectionType : null,
'inspection_reason' => trim((string) ($context['inspection_reason'] ?? '')) ?: null,
'return_waybill_no' => $returnWaybills === [] ? null : mb_substr((string) array_key_first($returnWaybills), 0, 191),
'settlement_weight' => $settlementWeight > 0 ? round($settlementWeight, 2) : null,
];
if ($importedCourier !== '' && $contextCourier !== '' && $importedCourier !== $contextCourier) {
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:该运单实际快递为「' . $contextCourier . '」,与导入的「' . $importedCourier . '」不一致', null);
}
if ($inspectionType !== '扣单') {
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:该运单在查件工单里不是「扣单」类型', null);
}
$rules = $type === '拦截费' ? ($quoteRules['intercept'] ?? []) : ($quoteRules['return'] ?? []);
$rule = null;
foreach ($rules as $candidate) {
if ($candidate['courier'] === $courier) {
$rule = $candidate;
break;
}
}
if ($rule === null) {
$modes = array_values(array_unique($ruleModes[$courier][$type] ?? []));
if ($modes !== []) {
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:快递「' . $courier . '」的' . $type . '报价规则(' . implode('、', $modes) . ')暂不支持自动核算', null);
}
return $this->importQuoteRow($pair, $fields, null, '未匹配报价',
'不结:快递「' . ($courier !== '' ? $courier : '未知') . '」没有启用的「' . $type . '」报价规则', null);
}
if ($type === '拦截费') {
return $this->importQuoteRow($pair, $fields, (float) $rule['value'], '已计算', '',
'规则 #' . $rule['id'] . ';查件扣单按固定单价 ' . $this->trimNumber((float) $rule['value']) . ' 元/单');
}
$evidenceParts = ['规则 #' . $rule['id']];
if ($hasReturn) {
$evidenceParts[] = '售后退货退款已进仓,结算重量 ' . $this->trimNumber($settlementWeight) . 'kg';
} else {
$evidenceParts[] = '售后退货退款无进仓时间/实退数量';
}
if ($rule['mode'] === '固定单价') {
return $this->importQuoteRow(
$pair,
$fields,
$hasReturn ? (float) $rule['value'] : 0.0,
'已计算',
$hasReturn ? '' : '不结:售后退货退款无进仓时间/实退数量',
implode(';', $evidenceParts),
);
}
if (! $hasReturn) {
return $this->importQuoteRow($pair, $fields, 0.0, '已计算',
'不结:售后退货退款无进仓时间/实退数量', implode(';', $evidenceParts));
}
if ($settlementWeight <= 0) {
return $this->importQuoteRow($pair, $fields, null, '待核价',
'待核价:商品资料缺少重量,暂无法按重量核算退回费', implode(';', $evidenceParts));
}
return $this->importQuoteRow(
$pair,
$fields,
round($settlementWeight * (float) $rule['value'], 2),
'已计算',
'',
implode(';', $evidenceParts) . ';' . $this->trimNumber($settlementWeight) . 'kg × '
. $this->trimNumber((float) $rule['value']) . ' 元/kg',
);
}
/**
* @param array{row_number:int,waybill:string,surcharge_type:string,courier_name:string} $pair
* @param array<string,mixed> $fields
* @return array<string,mixed>
*/
private function importQuoteRow(
array $pair,
array $fields,
?float $amount,
string $status,
string $reason,
?string $evidence,
): array {
$decision = $amount !== null && $amount > 0 ? '正常结' : '不结';
return array_merge([
'row_number' => $pair['row_number'],
'waybill' => $pair['waybill'],
'surcharge_type' => $pair['surcharge_type'],
'decision' => $decision,
'reason' => $decision === '正常结'
? ''
: ($reason !== '' ? $reason : '结算金额为 0'),
], $fields, [
'settlement_amount' => $amount,
'settlement_status' => $status,
'evidence' => $evidence,
]);
}
/**
* 当前启用的报价规则里,可用于导入核算的部分。
*
* @return array{overlength:list<array{id:int,courier:string,threshold:float,amount:float}>,
* return:list<array{id:int,courier:string,mode:string,value:float}>,
* intercept:list<array{id:int,courier:string,value:float}>}
*/
private function importQuoteRules(): array
{
$rules = ['overlength' => [], 'return' => [], 'intercept' => []];
try {
$rows = DB::table('business_courier_surcharge_quotes')
->where('status', 'active')
->orderBy('id')
->get();
} catch (Throwable) {
return $rules;
}
foreach ($rows as $rule) {
$types = json_decode((string) $rule->types, true) ?: [];
$courier = trim((string) $rule->courier);
$mode = trim((string) $rule->mode);
$condition = (string) $rule->condition;
$standard = (string) $rule->standard;
if ($courier === '') {
continue;
}
if (in_array('超长费', $types, true)
&& (str_contains($mode, '按尺寸') || str_contains($mode, '最长边'))
&& preg_match('/单边超\s*(\d+(?:\.\d+)?)\s*(?:cm|厘米|CM|㎝)/u', $condition, $threshold) === 1
&& preg_match('/(\d+(?:\.\d+)?)\s*元\s*\/?\s*单/u', $standard, $price) === 1) {
$rules['overlength'][] = [
'id' => (int) $rule->id,
'courier' => $courier,
'threshold' => (float) $threshold[1],
'amount' => (float) $price[1],
];
}
if (! str_contains($condition, '扣单')) {
continue;
}
if (in_array('拦截费', $types, true)
&& $mode === '固定单价'
&& preg_match('/(\d+(?:\.\d+)?)\s*(?:元)?\s*\/?\s*单/u', $standard, $price) === 1) {
$rules['intercept'][] = [
'id' => (int) $rule->id,
'courier' => $courier,
'value' => (float) $price[1],
];
}
if (in_array('退回费', $types, true)) {
if ($mode === '固定单价'
&& preg_match('/(\d+(?:\.\d+)?)\s*(?:元)?\s*\/?\s*单/u', $standard, $price) === 1) {
$rules['return'][] = [
'id' => (int) $rule->id,
'courier' => $courier,
'mode' => '固定单价',
'value' => (float) $price[1],
];
} elseif ($mode === '按重量'
&& preg_match('/(\d+(?:\.\d+)?)\s*元\s*\/\s*(?:kg|KG|公斤|千克)/u', $standard, $price) === 1) {
$rules['return'][] = [
'id' => (int) $rule->id,
'courier' => $courier,
'mode' => '按重量',
'value' => (float) $price[1],
];
}
}
}
return $rules;
}
/**
* @param list<string> $skuIds
* @return array<string,array{l:float,w:float,h:float}>
*/
private function importDimensions(array $skuIds): array
{
$map = [];
foreach (array_chunk(array_values(array_unique(array_filter($skuIds))), 500) as $chunk) {
try {
$rows = $this->connection->table('ops_jst_sku')
->whereIn('sku_id', $chunk)
->get(['sku_id', 'l', 'w', 'h']);
} catch (Throwable) {
return $map;
}
foreach ($rows as $row) {
$length = (float) $row->l;
$width = (float) $row->w;
$height = (float) $row->h;
if ($length > 0 && $width > 0 && $height > 0) {
$map[trim((string) $row->sku_id)] = ['l' => $length, 'w' => $width, 'h' => $height];
}
}
}
return $map;
}
private function trimNumber(float $value): string
{
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
/**
* @param array{row_number:int,waybill:string,surcharge_type:string,courier_name:string} $pair
* @param array<string,array{courier:string}> $returnContext
* @param array<string,array<string,list<string>>> $ruleModes
*/
private function missingReason(
array $pair,
array $returnContext,
array $ruleModes,
bool $contextComplete,
string $mismatchedCourier = '',
): string {
$courier = $pair['courier_name'];
$type = $pair['surcharge_type'];
if ($pair['waybill'] === '') {
return '单号为空';
}
if ($type === '') {
return '附加费类型为空';
}
if ($mismatchedCourier !== '') {
return '不结:该运单+类型匹配到快递「' . $mismatchedCourier . '」,与导入的快递「' . $courier . '」不一致';
}
if ($courier !== '') {
$modes = array_values(array_unique($ruleModes[$courier][$type] ?? []));
if ($modes === []) {
return '不结:快递「' . $courier . '」没有启用的「' . $type . '」报价规则';
}
if ($type === '退回费') {
if (array_intersect($modes, ['固定单价', '按重量']) === []) {
return '不结:快递「' . $courier . '」的退回费规则(' . implode('、', $modes) . ')暂不支持自动核算';
}
$context = $returnContext[$pair['waybill']] ?? null;
$inspectionType = trim((string) ($context['inspection_type'] ?? ''));
if ($inspectionType !== '' && $inspectionType !== '扣单') {
return '不结:查件工单类型是「' . $inspectionType . '」(' . trim((string) ($context['inspection_reason'] ?? '')) . '),不是扣单,不满足生效条件';
}
if ($context !== null) {
return '不结:快递「' . $courier . '」有启用的退回费规则,但该运单未生成结算行(不在已拉取范围或结算数据尚未同步)';
}
return '不结:查件工单中没有该运单的扣单记录';
}
if ($type === '拦截费') {
if (array_intersect($modes, ['固定单价']) === []) {
return '不结:快递「' . $courier . '」的拦截费规则(' . implode('、', $modes) . ')暂不支持自动核算';
}
$context = $returnContext[$pair['waybill']] ?? null;
$inspectionType = trim((string) ($context['inspection_type'] ?? ''));
if ($inspectionType !== '' && $inspectionType !== '扣单') {
return '不结:查件工单类型是「' . $inspectionType . '」(' . trim((string) ($context['inspection_reason'] ?? '')) . '),不是扣单,不满足生效条件';
}
if ($context !== null) {
return '不结:快递「' . $courier . '」有启用的拦截费规则,但该运单未生成结算行(不在已拉取范围或结算数据尚未同步)';
}
return '不结:查件工单中没有该运单的扣单记录';
}
if ($type === '超长费') {
return '不结:快递「' . $courier . '」有启用的超长费规则,但该运单未达条件或不在已拉取范围';
}
return '不结:快递「' . $courier . '」有启用规则(' . implode('、', $modes) . '),但「' . $type . '」暂未接入自动核算';
}
// 未提供快递名称时,沿用查件工单兜底原因。
if ($type === '退回费') {
if (! $contextComplete) {
return '不结:该运单没有查件扣单记录,或快递/规则不在已拉取范围';
}
$contextCourier = trim((string) ($returnContext[$pair['waybill']]['courier'] ?? ''));
if ($contextCourier === '') {
return '不结:查件工单中没有该运单的扣单记录';
}
$inspectionType = trim((string) ($returnContext[$pair['waybill']]['inspection_type'] ?? ''));
if ($inspectionType !== '' && $inspectionType !== '扣单') {
return '不结:查件工单类型是「' . $inspectionType . '」(' . trim((string) ($returnContext[$pair['waybill']]['inspection_reason'] ?? '')) . '),不是扣单,不满足生效条件';
}
$modes = array_values(array_unique($ruleModes[$contextCourier]['退回费'] ?? []));
if ($modes === []) {
return '不结:快递「' . $contextCourier . '」没有启用的退回费报价规则';
}
if (array_intersect($modes, ['固定单价', '按重量']) === []) {
return '不结:快递「' . $contextCourier . '」的退回费规则(' . implode('、', $modes) . ')暂不支持自动核算';
}
return '不结:该运单未生成退回费结算行(不在已拉取范围或结算数据尚未同步)';
}
if ($type === '拦截费') {
if (! $contextComplete) {
return '不结:该运单没有查件扣单记录,或快递/规则不在已拉取范围';
}
$contextCourier = trim((string) ($returnContext[$pair['waybill']]['courier'] ?? ''));
if ($contextCourier === '') {
return '不结:查件工单中没有该运单的扣单记录';
}
$inspectionType = trim((string) ($returnContext[$pair['waybill']]['inspection_type'] ?? ''));
if ($inspectionType !== '' && $inspectionType !== '扣单') {
return '不结:查件工单类型是「' . $inspectionType . '」(' . trim((string) ($returnContext[$pair['waybill']]['inspection_reason'] ?? '')) . '),不是扣单,不满足生效条件';
}
$modes = array_values(array_unique($ruleModes[$contextCourier]['拦截费'] ?? []));
if ($modes === []) {
return '不结:快递「' . $contextCourier . '」没有启用的拦截费报价规则';
}
if (array_intersect($modes, ['固定单价']) === []) {
return '不结:快递「' . $contextCourier . '」的拦截费规则(' . implode('、', $modes) . ')暂不支持自动核算';
}
return '不结:该运单未生成拦截费结算行(不在已拉取范围或结算数据尚未同步)';
}
if ($type === '超长费') {
return '不结:未找到超长费结算行,单边未超报价阈值或不在已拉取范围';
}
return '不结:该附加费类型暂未接入自动核算';
}
/**
* 未匹配到结算行时,回查查件工单补齐快递、商品、店铺、线上下单号等信息。
*
* @param list<string> $waybills
* @return array<string,array<string,mixed>>
*/
private function deductOrderContext(array $waybills): array
{
$waybills = array_values(array_unique(array_filter(
array_map(static fn ($waybill): string => trim((string) $waybill), $waybills),
static fn (string $waybill): bool => $waybill !== '',
)));
if ($waybills === [] || count($waybills) > self::IMPORT_RETURN_CONTEXT_LIMIT) {
return [];
}
$context = [];
foreach (array_chunk($waybills, 50) as $chunk) {
try {
$rows = $this->connection->table('js_jst_work_order_info')
->where('title', '查件')
->where(function ($query) use ($chunk): void {
foreach ($chunk as $waybill) {
$query->orWhere('businessFieldList', 'like', '%' . str_replace(['%', '_'], '', $waybill) . '%');
}
})
->orderByDesc('Id')
->get(['businessFieldList']);
} catch (Throwable) {
continue;
}
foreach ($rows as $row) {
$fields = json_decode((string) $row->businessFieldList, true);
if (! is_array($fields)) {
continue;
}
$flat = [];
foreach ($fields as $field) {
if (is_array($field) && isset($field['fieldId'])) {
$flat[(string) $field['fieldId']] = $field;
}
}
$waybill = trim((string) ($flat['lIdField']['value'] ?? ''));
$cascade = trim((string) ($flat[self::CHECK_TYPE_FIELD]['value'] ?? ''));
if ($waybill === '') {
continue;
}
if (isset($context[$waybill]) && ($context[$waybill]['inspection_type'] ?? '') === '扣单') {
continue;
}
[$inspectionType, $inspectionReason] = array_pad(explode('/', $cascade, 2), 2, '');
$items = [];
foreach (($flat['itemInfo']['childFieldList'] ?? []) as $group) {
if (! is_array($group)) {
continue;
}
$item = [];
foreach ($group as $field) {
if (is_array($field) && isset($field['fieldId'])) {
$item[(string) $field['fieldId']] = $field['value'] ?? null;
}
}
$skuId = strtoupper(trim((string) ($item['skuId'] ?? '')));
if ($skuId === '') {
continue;
}
$items[] = [
'sku_id' => $skuId,
'qty' => (float) ($item['qty'] ?? 0),
'platform_product_id' => trim((string) ($item['platformItemId'] ?? '')),
'i_id' => trim((string) ($item['iid'] ?? '')),
];
}
$context[$waybill] = [
'courier' => trim((string) ($flat['logisticsField']['value'] ?? '')),
'o_id' => (int) ($flat['o_id']['value'] ?? 0),
'online_order_no' => trim((string) ($flat['orderField']['value'] ?? '')),
'shop_name' => trim((string) ($flat['shopField']['valueLabel'] ?? ($flat['shopField']['value'] ?? ''))),
'inspection_type' => trim((string) $inspectionType),
'inspection_reason' => trim((string) $inspectionReason),
'send_time' => trim((string) ($flat['orderSendTime']['value'] ?? '')),
'items' => $items,
];
}
}
return $context;
}
/**
* @param list<array<string,mixed>> $items
* @return array{sku_id:string,i_id:string,platform_product_id:string}
*/
private function primaryImportItem(array $items): array
{
foreach ($items as $item) {
if (trim((string) ($item['platform_product_id'] ?? '')) !== '') {
return [
'sku_id' => strtoupper(trim((string) ($item['sku_id'] ?? ''))),
'i_id' => trim((string) ($item['i_id'] ?? '')),
'platform_product_id' => trim((string) $item['platform_product_id']),
];
}
}
$first = $items[0] ?? [];
return [
'sku_id' => strtoupper(trim((string) ($first['sku_id'] ?? ''))),
'i_id' => trim((string) ($first['i_id'] ?? '')),
'platform_product_id' => trim((string) ($first['platform_product_id'] ?? '')),
];
}
/**
* @param list<array<string,mixed>> $items
* @param array<string,float> $weights
*/
private function importWeightSum(array $items, array $weights): ?float
{
$total = 0.0;
$found = false;
foreach ($items as $item) {
$skuId = strtoupper(trim((string) ($item['sku_id'] ?? '')));
$weight = $skuId !== '' ? ($weights[$skuId] ?? null) : null;
if ($weight !== null) {
$found = true;
$total += (float) ($item['qty'] ?? 0) * $weight;
}
}
return $found ? round($total, 2) : null;
}
/**
* @param list<string> $skuIds
* @return array<string,float>
*/
private function importWeights(array $skuIds): array
{
$map = [];
foreach (array_chunk($skuIds, 500) as $chunk) {
try {
$rows = $this->connection->table('ops_jst_sku')
->whereIn('sku_id', $chunk)
->get(['sku_id', 'weight']);
} catch (Throwable) {
return [];
}
foreach ($rows as $row) {
$skuId = strtoupper(trim((string) $row->sku_id));
$weight = $row->weight;
if ($skuId === '' || $weight === null || $weight === '' || isset($map[$skuId])) {
continue;
}
$map[$skuId] = (float) $weight;
}
}
return $map;
}
/**
* @param array<string,array<string,mixed>> $returnContext
* @return array<int,list<array{o_id:int,l_id:string,sku_id:string,r_qty:float,receive_date:string}>>
*/
private function importReturns(array $returnContext): array
{
$oIds = [];
foreach ($returnContext as $context) {
$oId = (int) ($context['o_id'] ?? 0);
if ($oId > 0) {
$oIds[$oId] = true;
}
}
if ($oIds === []) {
return [];
}
try {
$upstream = app(UpstreamReadOnlyQueryFactory::class)->forSource('operations_replica');
} catch (Throwable) {
return [];
}
$byOrder = [];
foreach (array_chunk(array_keys($oIds), 200) as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
try {
$rows = $upstream->select(
'SELECT a.o_id, a.l_id, ri.sku_id, ri.r_qty, '
. 'CONVERT(varchar(19), ri.receive_date, 120) AS receive_date '
. 'FROM rpt.jst_returnAndRefundInfo a '
. 'INNER JOIN rpt.jst_returnAndRefundItem ri ON ri.as_id = a.as_id '
. 'WHERE a.o_id IN (' . $placeholders . ')',
$chunk,
);
} catch (Throwable) {
continue;
}
foreach ($rows as $row) {
$row = (array) $row;
$oId = (int) ($row['o_id'] ?? 0);
$waybill = trim((string) ($row['l_id'] ?? ''));
if ($oId <= 0
|| $waybill === ''
|| (float) ($row['r_qty'] ?? 0) <= 0
|| trim((string) ($row['receive_date'] ?? '')) === '') {
continue;
}
$byOrder[$oId][] = [
'o_id' => $oId,
'l_id' => $waybill,
'sku_id' => trim((string) ($row['sku_id'] ?? '')),
'r_qty' => (float) ($row['r_qty'] ?? 0),
'receive_date' => trim((string) ($row['receive_date'] ?? '')),
];
}
}
return $byOrder;
}
/** @return array<string,array<string,list<string>>> */
private function activeRuleModes(): array
{
try {
$rules = DB::table('business_courier_surcharge_quotes')
->where('status', 'active')
->get(['courier', 'types', 'mode']);
} catch (Throwable) {
return [];
}
$map = [];
foreach ($rules as $rule) {
$types = json_decode((string) $rule->types, true) ?: [];
$courier = trim((string) $rule->courier);
if ($courier === '') {
continue;
}
foreach ($types as $type) {
$type = trim((string) $type);
if ($type !== '') {
$map[$courier][$type][] = trim((string) $rule->mode);
}
}
}
return $map;
}
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
}
/** @return array<string,mixed> */
private function mapRow(object $row): array
{
return [
'courier_name' => $row->courier_name,
'waybill' => $row->waybill,
'shipped_at' => $row->shipped_at,
'platform_product_id' => $row->platform_product_id,
'product_code' => $row->product_code,
'shop_name' => $row->shop_name,
'online_order_no' => $row->online_order_no,
'length' => $row->length,
'width' => $row->width,
'height' => $row->height,
'volume' => $row->volume,
'inspection_type' => $row->inspection_type,
'inspection_reason' => $row->inspection_reason,
'return_waybill_no' => $row->return_waybill_no,
'settlement_weight' => $row->settlement_weight,
'settlement_amount' => $row->settlement_amount,
'surcharge_type' => $row->surcharge_type,
];
}
private function timestamp(mixed $value): ?int
{
if (! is_string($value) || $value === '') {
return null;
}
try {
return Carbon::parse($value)->getTimestamp();
} catch (Throwable) {
return null;
}
}
}