function valueAt(record, path) { return path.split('.').reduce((value, key) => value?.[key], record) } export function isPositiveId(value) { return /^\d+$/.test(String(value ?? '')) && BigInt(String(value)) > 0n } export function isNonBlank(value) { return typeof value === 'string' && value.trim().length > 0 } export function isIsoDateTime(value) { return isNonBlank(value) && Number.isFinite(Date.parse(value)) } export function isNonNegativeNumber(value) { return value !== null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 } function gaps(record, rules) { return Object.entries(rules) .filter(([path, predicate]) => !predicate(valueAt(record, path), record)) .map(([path]) => path) } export function containerFieldGaps(record, { requireContainerNo = false } = {}) { return gaps(record, { id: isPositiveId, cabinetNo: isNonBlank, warehouseId: isPositiveId, warehouseName: isNonBlank, channelGroupCode: isNonBlank, containerType: isNonBlank, capacityCbm: (value) => Number(value) > 0, maxWeightKg: (value) => Number(value) > 0, planStatus: isNonBlank, ...(requireContainerNo ? { containerNo: isNonBlank } : {}), }) } export function preloadFieldGaps(record) { return gaps(record, { planId: isPositiveId, containerBookingId: isPositiveId, preloadNo: isNonBlank, cabinetNo: isNonBlank, customers: (value) => Array.isArray(value) && value.length > 0 && value.every((customer) => isPositiveId(customer?.accountId) && isNonBlank(customer?.customerName)), warehouseName: isNonBlank, containerType: isNonBlank, status: isNonBlank, skuCount: isNonNegativeNumber, totalPackageCount: isNonNegativeNumber, totalVolumeCbm: isNonNegativeNumber, totalGrossWeightKg: isNonNegativeNumber, createdAt: isIsoDateTime, }) } export function loadingListFieldGaps(record) { return gaps(record, { planId: isPositiveId, containerBookingId: isPositiveId, loadingListNo: isNonBlank, containerNo: isNonBlank, cabinetNo: isNonBlank, warehouseName: isNonBlank, status: (value) => value === 'COMPLETED', totalPackageCount: isNonNegativeNumber, totalGrossWeightKg: isNonNegativeNumber, totalNetWeightKg: isNonNegativeNumber, totalVolumeCbm: isNonNegativeNumber, totalQuantity: isNonNegativeNumber, createdAt: isIsoDateTime, }) } export function oceanBookingFieldGaps(record) { return gaps(record, { id: isPositiveId, bookingNo: isNonBlank, status: (value) => ['DRAFT', 'RELEASED'].includes(value), vesselName: isNonBlank, voyageNo: isNonBlank, estimatedSailingAt: isIsoDateTime, estimatedArrivalAt: isIsoDateTime, customerName: (value, item) => item.customerId == null ? value == null : isNonBlank(value), shippingCompanyName: (value, item) => item.shippingCompanyId == null ? value == null : isNonBlank(value), bookingAgentName: (value, item) => item.bookingAgentId == null ? value == null : isNonBlank(value), }) } export function portScheduleFieldGaps(record) { const view = record?.portSchedule ?? record const schedule = view?.schedule ?? view return gaps(schedule, { id: isPositiveId, scheduleNo: isNonBlank, sourceName: isNonBlank, vesselName: isNonBlank, voyageNo: isNonBlank, departurePortCode: isNonBlank, destinationPortCode: isNonBlank, etdAt: isIsoDateTime, etaAt: isIsoDateTime, observedAt: isIsoDateTime, updatedAt: isIsoDateTime, expiresAt: isIsoDateTime, }).concat(typeof view?.expired === 'boolean' ? [] : ['expired']) } export function truckAppointmentFieldGaps(record) { const result = gaps(record, { id: isPositiveId, appointmentNo: isNonBlank, oceanBookingId: isPositiveId, warehouseId: (value) => value == null || isPositiveId(value), plannedLoadingAt: isIsoDateTime, status: isNonBlank, carrierId: isPositiveId, carrierName: isNonBlank, payableAmount: isNonNegativeNumber, currency: (value) => value === 'RMB' || value === 'USD', attachmentCount: isNonNegativeNumber, }) for (const field of ['containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo', 'containers']) { if (Object.hasOwn(record || {}, field)) result.push(`forbidden:${field}`) } return result } export function customsCaseFieldGaps(record) { const customsCase = record?.customsCase ?? record?.case ?? record return gaps(customsCase, { id: isPositiveId, customsNo: isNonBlank, orderId: isPositiveId, inboundId: isPositiveId, containerBookingId: isPositiveId, checklistVersionId: isPositiveId, checklistCode: isNonBlank, checklistVersionNo: isNonBlank, status: isNonBlank, updatedAt: isIsoDateTime, }) } export function outboundAggregateFieldGaps(record) { const aggregate = record?.aggregate ?? record const result = gaps(aggregate, { containerBookingId: isPositiveId, containerNo: isNonBlank, loadingPlanId: isPositiveId, loadingPlanStatus: isNonBlank, oceanBookingId: isPositiveId, oceanBookingStatus: isNonBlank, dispatchStatus: isNonBlank, version: isNonNegativeNumber, inbounds: (value) => Array.isArray(value) && value.length > 0, 'gate.inbounds': (value) => Array.isArray(value) && value.length > 0, }) for (const [index, inbound] of (Array.isArray(aggregate?.inbounds) ? aggregate.inbounds : []).entries()) { result.push(...gaps(inbound, { orderId: isPositiveId, inboundId: isPositiveId, inboundCode: isNonBlank, tenantId: isPositiveId, inboundStatus: isNonBlank, }).map((field) => `inbounds[${index}].${field}`)) } return result }