import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import crypto from 'crypto';
import {
SFBaseResponse,
SFExpressConfig,
SFExpressError,
CreateOrderRequest,
CreateOrderResponse,
TrackShipmentRequest,
TrackShipmentResponse,
QueryRoutesRequest,
QueryRoutesResponse,
ServiceInquiryRequest,
ServiceInquiryResponse,
LogisticsServicesRequest,
LogisticsServicesResponse
} from './types.js';
export class SFExpressClient {
private axiosInstance: AxiosInstance;
private config: SFExpressConfig;
constructor(config: SFExpressConfig) {
this.config = config;
this.axiosInstance = axios.create({
baseURL: config.apiUrl,
timeout: config.timeout || 30000,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json'
}
});
// Add request interceptor for authentication
this.axiosInstance.interceptors.request.use(
(config) => this.addAuthentication(config),
(error) => Promise.reject(error)
);
// Add response interceptor for error handling
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => this.handleError(error)
);
}
/**
* Add SF Express authentication to the request
*/
private addAuthentication(config: AxiosRequestConfig): AxiosRequestConfig {
const timestamp = Date.now().toString();
const msgData = JSON.stringify(config.data || {});
// Create signature for SF Express API
const signString = `${msgData}${timestamp}${this.config.checkWord}`;
const msgDigest = crypto.createHash('md5').update(signString, 'utf8').digest('base64');
// Add authentication parameters
config.data = {
partnerID: this.config.partnerID,
requestID: this.config.requestID,
serviceCode: this.getServiceCode(config.url || ''),
timestamp,
msgDigest,
msgData
};
return config;
}
/**
* Get service code based on the API endpoint
*/
private getServiceCode(url: string): string {
if (url.includes('create-order')) return 'EXP_RECE_CREATE_ORDER';
if (url.includes('track-shipment')) return 'EXP_RECE_SEARCH_ORDER_RESP';
if (url.includes('query-routes')) return 'EXP_RECE_SEARCH_ROUTES';
if (url.includes('service-inquiry')) return 'EXP_RECE_SEARCH_SERVICE';
if (url.includes('logistics-services')) return 'EXP_RECE_SEARCH_LOGISTICS';
return 'UNKNOWN_SERVICE';
}
/**
* Handle API errors
*/
private handleError(error: any): Promise<never> {
if (error.response) {
const { status, data } = error.response;
const errorMsg = data?.errorMsg || data?.message || 'API request failed';
const errorCode = data?.errorCode || status.toString();
throw new SFExpressError(
`SF Express API Error: ${errorMsg}`,
errorCode,
{ status, data }
);
} else if (error.request) {
throw new SFExpressError(
'Network error: No response from SF Express API',
'NETWORK_ERROR',
{ originalError: error.message }
);
} else {
throw new SFExpressError(
`Request configuration error: ${error.message}`,
'CONFIG_ERROR',
{ originalError: error.message }
);
}
}
/**
* Make authenticated API request
*/
private async makeRequest<T>(
endpoint: string,
data: any
): Promise<SFBaseResponse<T>> {
try {
const response = await this.axiosInstance.post(endpoint, data);
if (!response.data.success) {
throw new SFExpressError(
response.data.errorMsg || 'API request failed',
response.data.errorCode || 'API_ERROR',
response.data
);
}
return response.data;
} catch (error) {
if (error instanceof SFExpressError) {
throw error;
}
throw new SFExpressError(
'Unexpected error occurred',
'UNKNOWN_ERROR',
{ originalError: error }
);
}
}
/**
* Create a new shipping order (Category 1, apiClassify 1)
*/
async createOrder(request: CreateOrderRequest): Promise<CreateOrderResponse> {
const response = await this.makeRequest<CreateOrderResponse>(
'/create-order',
request
);
if (!response.data) {
throw new SFExpressError('No data returned from create order API', 'NO_DATA');
}
return response.data;
}
/**
* Track shipment status (Category 1, apiClassify 2)
*/
async trackShipment(request: TrackShipmentRequest): Promise<TrackShipmentResponse[]> {
const response = await this.makeRequest<TrackShipmentResponse[]>(
'/track-shipment',
request
);
if (!response.data) {
throw new SFExpressError('No data returned from track shipment API', 'NO_DATA');
}
return Array.isArray(response.data) ? response.data : [response.data];
}
/**
* Query available routes and services (Category 1, apiClassify 3)
*/
async queryRoutes(request: QueryRoutesRequest): Promise<QueryRoutesResponse> {
const response = await this.makeRequest<QueryRoutesResponse>(
'/query-routes',
request
);
if (!response.data) {
throw new SFExpressError('No data returned from query routes API', 'NO_DATA');
}
return response.data;
}
/**
* Inquire about service availability (Category 1, apiClassify 4)
*/
async serviceInquiry(request: ServiceInquiryRequest): Promise<ServiceInquiryResponse> {
const response = await this.makeRequest<ServiceInquiryResponse>(
'/service-inquiry',
request
);
if (!response.data) {
throw new SFExpressError('No data returned from service inquiry API', 'NO_DATA');
}
return response.data;
}
/**
* Query logistics services (Category 6, apiClassify 2)
*/
async getLogisticsServices(request: LogisticsServicesRequest): Promise<LogisticsServicesResponse> {
const response = await this.makeRequest<LogisticsServicesResponse>(
'/logistics-services',
request
);
if (!response.data) {
throw new SFExpressError('No data returned from logistics services API', 'NO_DATA');
}
return response.data;
}
/**
* Test connection to SF Express API
*/
async testConnection(): Promise<boolean> {
try {
// Use a simple service inquiry to test the connection
await this.serviceInquiry({
originCode: '010', // Beijing
destCode: '021' // Shanghai
});
return true;
} catch (error) {
console.error('SF Express API connection test failed:', error);
return false;
}
}
/**
* Get client configuration (without sensitive data)
*/
getConfig(): Partial<SFExpressConfig> {
return {
apiUrl: this.config.apiUrl,
partnerID: this.config.partnerID,
requestID: this.config.requestID,
timeout: this.config.timeout
// checkWord is intentionally omitted for security
};
}
}