CaiWuXiTong/modules/EcommerceFinance/Repositories/CourierSurchargeSettlementMirrorRepository.php

995 lines
36 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 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();
$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->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,string> $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'] ?? ''));
$returnWaybillNo = $contextReturns[(int) ($context['o_id'] ?? 0)] ?? null;
$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,
];
}
/**
* @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,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.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][$waybill] = true;
}
}
$map = [];
foreach ($byOrder as $oId => $waybills) {
$map[(int) $oId] = implode(',', array_keys($waybills));
}
return $map;
}
/** @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;
}
}
}