import { Injectable } from '@nestjs/common'; import { CourierProviderCode } from '../courier.constants'; import { CourierApiError } from '../courier.error'; import type { BatchShipmentQuery, CourierCallOptions, CreateShipmentInput, CreateShipmentResult, DeliveryCoverageResult, FreightEstimateResult, ICourierProvider, ShipmentDetail, ShipmentQuery, TrackCallbackResponse, TrackCallbackPayload, TrackNode, CourierMappedOrderStatus, } from '../courier.types'; import { XiaofeixiaClient } from './xiaofeixia.client'; import { XIAOFEIXIA_CMD } from './xiaofeixia.constants'; import type { XiaofeixiaCreateOrderData, XiaofeixiaDeliveryCoverageData, XiaofeixiaFreightEstimateData, XiaofeixiaOrderDetail, XiaofeixiaTrackNode, } from './xiaofeixia.types'; @Injectable() export class XiaofeixiaProvider implements ICourierProvider { readonly code = CourierProviderCode.XIAOFEIXIA; constructor(private readonly client: XiaofeixiaClient) {} async createShipment(input: CreateShipmentInput, options?: CourierCallOptions): Promise { const data = await this.client.request( XIAOFEIXIA_CMD.CREATE_ORDER, { customerId: input.customerId, outNumber: input.outNumber, fromAddress: input.from.address, fromAddressDetail: input.from.addressDetail, fromCoordinate: this.formatCoordinate(input.from.coordinate), fromMobile: input.from.mobile, fromName: input.from.name, toAddress: input.to.address, toAddressDetail: input.to.addressDetail, toCoordinate: this.formatCoordinate(input.to.coordinate), toMobile: input.to.mobile, toName: input.to.name, goodsName: input.goodsName, goodsNum: input.goodsNum, weight: input.weight, insuredSumPrice: input.insuredSumPrice, collectionPrice: input.collectionPrice, payMode: input.payMode, remark: input.remark, }, options?.xiaofeixia, ); return { providerShipmentId: data.id, trackingNumber: data.number, }; } async cancelShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise { this.assertShipmentQuery(query); await this.client.request( XIAOFEIXIA_CMD.CANCEL_ORDER, { number: query.trackingNumber, outNumber: query.outNumber, }, options?.xiaofeixia, ); } async getShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise { this.assertShipmentQuery(query); const data = await this.client.request( XIAOFEIXIA_CMD.GET_ORDER, { number: query.trackingNumber, outNumber: query.outNumber, }, options?.xiaofeixia, ); return this.mapOrderDetail(data); } async batchGetShipments(query: BatchShipmentQuery, options?: CourierCallOptions): Promise { const number = query.trackingNumbers?.join(','); const outNumber = query.outNumbers?.join(','); if (!number && !outNumber) { throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code); } const data = await this.client.request( XIAOFEIXIA_CMD.BATCH_GET_ORDER, { number, outNumber, }, options?.xiaofeixia, ); return (data ?? []).map((item) => this.mapOrderDetail(item)); } async getTrack(query: ShipmentQuery, options?: CourierCallOptions): Promise { this.assertShipmentQuery(query); const data = await this.client.request( XIAOFEIXIA_CMD.TRACK_ROUTE, { number: query.trackingNumber, outNumber: query.outNumber, }, options?.xiaofeixia, ); return data ?? []; } async getSignPhotos(query: ShipmentQuery, options?: CourierCallOptions): Promise { this.assertShipmentQuery(query); const data = await this.client.request( XIAOFEIXIA_CMD.SIGN_PHOTOS, { number: query.trackingNumber, outNumber: query.outNumber, }, options?.xiaofeixia, ); return (data ?? []).filter((item) => typeof item === 'string' && item.trim().length > 0); } parseTrackCallback(body: unknown): TrackCallbackPayload | null { if (!body || typeof body !== 'object') return null; const raw = body as Record; const outNumber = String(raw.outNumber ?? '').trim(); const trackingNumber = String(raw.number ?? raw.trackingNumber ?? '').trim(); const status = String(raw.status ?? '').trim(); const statusName = String(raw.statusName ?? '').trim(); const trackInfo = String(raw.trackInfo ?? '').trim(); const createTime = String(raw.createTime ?? '').trim(); if (!outNumber && !trackingNumber) return null; if (!status && !trackInfo) return null; return { outNumber, trackingNumber, status, statusName, trackInfo, createTime, }; } mapTrackStatus(status: string, statusName?: string): CourierMappedOrderStatus | null { const name = (statusName || '').trim(); if (status === '5' || /签收/.test(name)) { return 'COMPLETED'; } switch (String(status).trim()) { case '1': return 'OUT_WAREHOUSE'; case '2': case '3': case '4': return 'SHIPPING'; case '7': return null; default: return null; } } async checkDeliveryCoverage(toAddress: string, options?: CourierCallOptions): Promise { const data = await this.client.request( XIAOFEIXIA_CMD.DELIVERY_COVERAGE, { toAddress }, options?.xiaofeixia, ); return { arriveTime: data.arriveTime, siteName: data.name, siteId: data.id, }; } async estimateFreight(weight: number, options?: CourierCallOptions): Promise { const data = await this.client.request( XIAOFEIXIA_CMD.ESTIMATE_FREIGHT, { weight }, options?.xiaofeixia, ); return { freightPrice: data.freightPrice }; } buildTrackCallbackResponse(success = true): TrackCallbackResponse { return { code: success ? '100000' : '300000', message: success ? 'success' : 'fail', }; } private assertShipmentQuery(query: ShipmentQuery): void { if (!query.trackingNumber && !query.outNumber) { throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code); } } private formatCoordinate(coordinate?: { lng: number; lat: number }): string | undefined { if (!coordinate) return undefined; return JSON.stringify({ lng: coordinate.lng, lat: coordinate.lat }); } private mapOrderDetail(data: XiaofeixiaOrderDetail): ShipmentDetail { return { trackingNumber: data.number, toCarrierName: data.toCarrierName, toSiteName: data.toSiteName, toName: data.toName, toMobile: data.toMobile, toAddress: data.toAddress, toAddressDetail: data.toAddressDetail, fromCarrierName: data.fromCarrierName, fromSiteName: data.fromSiteName, fromName: data.fromName, fromMobile: data.fromMobile, fromAddress: data.fromAddress, fromAddressDetail: data.fromAddressDetail, weight: data.weight, payModeName: data.payModeName, freightPrice: data.freightPrice, insuredPrice: data.insuredPrice, collectionPrice: data.collectionPrice, sumPrice: data.sumPrice, goodsName: data.goodsName, remark: data.remark, }; } }