172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
import { Provide } from '@midwayjs/core';
|
|
import { Config } from '@midwayjs/decorator';
|
|
import axios from 'axios';
|
|
import { parseStringPromise, Builder } from 'xml2js';
|
|
import { ShipmentType } from '../enums/base.enum';
|
|
|
|
@Provide()
|
|
export class CanadaPostService {
|
|
@Config('canadaPost.url')
|
|
url;
|
|
|
|
@Config('canadaPost.username')
|
|
username;
|
|
|
|
@Config('canadaPost.password')
|
|
password;
|
|
|
|
@Config('canadaPost.customerNumber')
|
|
customerNumber;
|
|
|
|
@Config('canadaPost.contractId')
|
|
contractId;
|
|
|
|
private getAuth() {
|
|
return {
|
|
username: this.username,
|
|
password: this.password,
|
|
};
|
|
}
|
|
|
|
private getHeaders(accept = 'application/vnd.cpc.shipment-v8+xml') {
|
|
return {
|
|
Accept: accept,
|
|
'Content-Type': accept,
|
|
'Accept-language': 'en-CA',
|
|
};
|
|
}
|
|
|
|
private async parseXML(xml: string) {
|
|
return await parseStringPromise(xml, { explicitArray: false });
|
|
}
|
|
|
|
private buildXML(data: any, rootName?: string, namespace?: string): string {
|
|
const builder = new Builder({
|
|
headless: false,
|
|
xmldec: { version: '1.0', encoding: 'UTF-8' },
|
|
});
|
|
|
|
// 如果指定了根节点和命名空间
|
|
if (rootName && namespace) {
|
|
const xmlObj = {
|
|
[rootName]: {
|
|
$: { xmlns: namespace },
|
|
...data,
|
|
},
|
|
};
|
|
return builder.buildObject(xmlObj);
|
|
}
|
|
|
|
// 默认直接构建(用于 createShipment 这类已有完整结构)
|
|
return builder.buildObject(data);
|
|
}
|
|
|
|
private normalizeCanadaPostRates(rawData: any) {
|
|
const services = rawData['price-quotes']?.['price-quote'];
|
|
if (!services) return [];
|
|
|
|
const list = Array.isArray(services) ? services : [services];
|
|
return list.map(s => ({
|
|
carrier_name: 'Canada Post',
|
|
service_name: s['service-name'],
|
|
service_id: s['service-code'],
|
|
total: {
|
|
value: 100 * parseFloat(s['price-details']['due']),
|
|
currency: 'CAD',
|
|
},
|
|
transit_time_days: s['service-standard']?.['expected-transit-time'],
|
|
type: ShipmentType.CANADAPOST,
|
|
}));
|
|
}
|
|
|
|
/** 获取运费估价 */
|
|
async getRates(rateRequest: any) {
|
|
const xmlBody = this.buildXML(
|
|
rateRequest,
|
|
'mailing-scenario',
|
|
'http://www.canadapost.ca/ws/ship/rate-v4'
|
|
);
|
|
const url = `${this.url}/rs/ship/price`;
|
|
|
|
const res = await axios.post(url, xmlBody, {
|
|
auth: this.getAuth(),
|
|
headers: this.getHeaders('application/vnd.cpc.ship.rate-v4+xml'),
|
|
});
|
|
|
|
return this.normalizeCanadaPostRates(await this.parseXML(res.data));
|
|
}
|
|
|
|
private normalizeCanadaPostShipment(rawData: any) {
|
|
const shipment = rawData['shipment-info'];
|
|
return {
|
|
id: shipment['shipment-id'],
|
|
tracking_provider: 'Canada Post',
|
|
unique_id: shipment['shipment-id'],
|
|
state: 'waiting-for-transit',
|
|
primary_tracking_number: shipment['tracking-pin'],
|
|
tracking_url: `https://www.canadapost-postescanada.ca/track-reperage/en#/details/${shipment['tracking-pin']}`,
|
|
labels: [
|
|
{
|
|
url: shipment['links']['link']?.find(
|
|
link => link['$']['rel'] === 'label'
|
|
)?.['$']['href'],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/** 创建运单 */
|
|
async createShipment(shipmentRequest: any) {
|
|
const xmlBody = this.buildXML(
|
|
shipmentRequest,
|
|
'shipment',
|
|
'http://www.canadapost.ca/ws/shipment-v8'
|
|
);
|
|
const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment`;
|
|
|
|
const res = await axios.post(url, xmlBody, {
|
|
auth: this.getAuth(),
|
|
headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'),
|
|
});
|
|
|
|
return this.normalizeCanadaPostShipment(await this.parseXML(res.data));
|
|
}
|
|
|
|
/** 查询运单 */
|
|
async getShipment(pin: string) {
|
|
const url = `${this.url}/vis/track/pin/${pin}/summary`;
|
|
|
|
const res = await axios.get(url, {
|
|
auth: this.getAuth(),
|
|
headers: this.getHeaders('application/vnd.cpc.track-v2+xml'),
|
|
});
|
|
const shipment = await this.parseXML(res.data);
|
|
const eventType =
|
|
shipment['tracking-summary']['pin-summary']['event-type']?.toUpperCase();
|
|
|
|
return {
|
|
shipment: {
|
|
state:
|
|
eventType === 'INDUCTION'
|
|
? 'waiting-for-transit'
|
|
: eventType === 'DELIVERED'
|
|
? 'delivered'
|
|
: 'in-transit',
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 取消运单 */
|
|
async cancelShipment(shipmentId: number) {
|
|
const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment/${shipmentId}`;
|
|
|
|
const res = await axios.delete(url, {
|
|
auth: this.getAuth(),
|
|
headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'),
|
|
});
|
|
console.log(res);
|
|
|
|
return res.status === 200 || res.status === 204;
|
|
}
|
|
}
|