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 */ 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|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 $filters * @return array{data:list>,total:int,has_more:bool,summary:array,sync:array,state:array} */ 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 $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 $filters * @return \Generator */ 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 $pairs * @return list> */ 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 $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 */ 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> $returnContext * @param array>> $ruleModes * @param array $contextWeights * @param array $contextReturns * @return array */ 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 $returnContext * @param array>> $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 $waybills * @return array> */ 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> $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> $items * @param array $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 $skuIds * @return array */ 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> $returnContext * @return array */ 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>> */ 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 */ 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; } } }