forked from yoone/API
913 lines
34 KiB
TypeScript
913 lines
34 KiB
TypeScript
import { Inject, Provide, sleep } from '@midwayjs/core';
|
||
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||
import { Service } from '../entity/service.entity';
|
||
import { In, IsNull, Like, Repository } from 'typeorm';
|
||
import { ShippingAddress } from '../entity/shipping_address.entity';
|
||
|
||
import { Order } from '../entity/order.entity';
|
||
import { Shipment } from '../entity/shipment.entity';
|
||
import { ShipmentItem } from '../entity/shipment_item.entity';
|
||
import { OrderShipment } from '../entity/order_shipment.entity';
|
||
import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto';
|
||
import {
|
||
ErpOrderStatus,
|
||
OrderStatus,
|
||
ShipmentType,
|
||
StockRecordOperationType,
|
||
} from '../enums/base.enum';
|
||
// import { generateUniqueId } from '../utils/helper.util';
|
||
import { FreightcomService } from './freightcom.service';
|
||
import { StockRecord } from '../entity/stock_record.entity';
|
||
import { Stock } from '../entity/stock.entity';
|
||
import { plainToClass } from 'class-transformer';
|
||
import { WPService } from './wp.service';
|
||
// import { Product } from '../entity/product.entty';
|
||
import { ShippingDetailsDTO } from '../dto/freightcom.dto';
|
||
import { CanadaPostService } from './canadaPost.service';
|
||
import { OrderItem } from '../entity/order_item.entity';
|
||
import { OrderSale } from '../entity/order_sale.entity';
|
||
import { UniExpressService } from './uni_express.service';
|
||
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
|
||
import { StockPoint } from '../entity/stock_point.entity';
|
||
import { OrderService } from './order.service';
|
||
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
|
||
import { SiteService } from './site.service';
|
||
|
||
@Provide()
|
||
export class LogisticsService {
|
||
@InjectEntityModel(Service)
|
||
serviceModel: Repository<Service>;
|
||
|
||
@InjectEntityModel(ShippingAddress)
|
||
shippingAddressModel: Repository<ShippingAddress>;
|
||
|
||
@InjectEntityModel(Stock)
|
||
stockModel: Repository<Stock>;
|
||
|
||
@InjectEntityModel(Order)
|
||
orderModel: Repository<Order>;
|
||
|
||
@InjectEntityModel(StockPoint)
|
||
stockPointModel: Repository<StockPoint>
|
||
|
||
@InjectEntityModel(OrderSale)
|
||
orderSaleModel: Repository<OrderSale>;
|
||
|
||
@InjectEntityModel(Shipment)
|
||
shipmentModel: Repository<Shipment>;
|
||
|
||
@InjectEntityModel(ShipmentItem)
|
||
shipmentItemModel: Repository<ShipmentItem>;
|
||
|
||
@InjectEntityModel(OrderShipment)
|
||
orderShipmentModel: Repository<OrderShipment>;
|
||
|
||
@InjectEntityModel(OrderItem)
|
||
orderItem: Repository<OrderItem>;
|
||
|
||
@Inject()
|
||
freightcomService: FreightcomService;
|
||
|
||
@Inject()
|
||
canadaPostService: CanadaPostService;
|
||
|
||
@Inject()
|
||
uniExpressService: UniExpressService;
|
||
|
||
@Inject()
|
||
freightwavesService: FreightwavesService;
|
||
|
||
@Inject()
|
||
wpService: WPService;
|
||
|
||
@Inject()
|
||
orderService: OrderService;
|
||
|
||
@Inject()
|
||
dataSourceManager: TypeORMDataSourceManager;
|
||
|
||
@Inject()
|
||
private readonly siteService: SiteService;
|
||
|
||
async getServiceList(param: QueryServiceDTO) {
|
||
const { pageSize, current, carrier_name, isActive } = param;
|
||
const where: Record<string, any> = {};
|
||
if (carrier_name) where.carrier_name = Like(`%${carrier_name}%`);
|
||
if (isActive !== undefined) where.isActive = isActive;
|
||
const [items, total] = await this.serviceModel.findAndCount({
|
||
where,
|
||
skip: (current - 1) * pageSize,
|
||
take: pageSize,
|
||
});
|
||
return { items, total, current, pageSize };
|
||
}
|
||
|
||
async toggleServiceActive(id: string, isActive: boolean) {
|
||
const service = await this.serviceModel.findOne({ where: { id } });
|
||
if (!service) {
|
||
throw new Error('服务商不存在');
|
||
}
|
||
service.isActive = isActive;
|
||
return this.serviceModel.save(service);
|
||
}
|
||
|
||
async getActiveServices() {
|
||
const services = await this.serviceModel.find({
|
||
where: { isActive: true },
|
||
});
|
||
if (!services) {
|
||
return [];
|
||
}
|
||
return services.map(service => service.id);
|
||
}
|
||
|
||
async createShippingAddress(shippingAddress: ShippingAddress) {
|
||
return await this.shippingAddressModel.save(shippingAddress);
|
||
}
|
||
|
||
async updateShipmentState(shipment: Shipment) {
|
||
try {
|
||
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
|
||
console.log('updateShipmentState data:', data);
|
||
// huo
|
||
if (data.status === 'FAIL') {
|
||
throw new Error('获取运单状态失败,原因为' + data.ret_msg)
|
||
}
|
||
shipment.state = data.data[0].state;
|
||
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
|
||
shipment.finished = true;
|
||
}
|
||
this.shipmentModel.save(shipment);
|
||
return shipment.state;
|
||
} catch (error) {
|
||
throw error;
|
||
// throw new Error(`更新运单状态失败 ${error.message}`);
|
||
}
|
||
}
|
||
|
||
//"expressFinish": 0, //是否快递创建完成(1:完成 0:未完成,需要轮询 2:失败)
|
||
async updateFreightwavesShipmentState(shipment: Shipment) {
|
||
try {
|
||
const data = await this.freightwavesService.queryOrder({ shipOrderId: shipment.order_id.toString() });
|
||
console.log('updateFreightwavesShipmentState data:', data);
|
||
// huo
|
||
if (data.expressFinish === 2) {
|
||
throw new Error('获取运单状态失败,原因为' + data.expressFailMsg)
|
||
}
|
||
|
||
if (data.expressFinish === 0) {
|
||
shipment.state = '203';
|
||
shipment.finished = true;
|
||
}
|
||
|
||
this.shipmentModel.save(shipment);
|
||
return shipment.state;
|
||
} catch (error) {
|
||
throw error;
|
||
// throw new Error(`更新运单状态失败 ${error.message}`);
|
||
}
|
||
}
|
||
|
||
|
||
async updateShipmentStateById(id: number) {
|
||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
|
||
return this.updateShipmentState(shipment);
|
||
}
|
||
|
||
async updateShippingAddress(id: number, shippingAddress: ShippingAddress) {
|
||
const address = await this.shippingAddressModel.findOneBy({ id });
|
||
if (!address) {
|
||
throw new Error(`发货地址 ID ${id} 不存在`);
|
||
}
|
||
await this.shippingAddressModel.update(id, shippingAddress);
|
||
return await this.shippingAddressModel.findOneBy({ id });
|
||
}
|
||
|
||
async getShippingAddressList() {
|
||
return await this.shippingAddressModel.find();
|
||
}
|
||
|
||
async delShippingAddress(id: number) {
|
||
const address = await this.shippingAddressModel.findOneBy({ id });
|
||
if (!address) {
|
||
throw new Error(`发货地址 ID ${id} 不存在`);
|
||
}
|
||
const result = await this.shippingAddressModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// async saveTracking(
|
||
// orderId: number,
|
||
// shipment: Record<string, any>,
|
||
// data: ShipmentBookDTO
|
||
// ) {
|
||
// const order = await this.orderModel.findOneBy({ id: orderId });
|
||
// const orderTracking = this.orderTrackingModel.save({
|
||
// orderId: String(orderId),
|
||
// siteId: order.siteId,
|
||
// externalOrderId: order.externalOrderId,
|
||
|
||
// });
|
||
|
||
// }
|
||
|
||
async getRateList(details: ShippingDetailsDTO) {
|
||
details.destination.address.country = 'CA';
|
||
details.origin.address.country = 'CA';
|
||
const { request_id } = await this.freightcomService.getRateEstimate(
|
||
details
|
||
);
|
||
const rateRequest = {
|
||
'customer-number': this.canadaPostService.customerNumber,
|
||
'parcel-characteristics': {
|
||
weight: details?.packaging_properties?.packages?.reduce(
|
||
(cur, next) => cur + (next?.measurements?.weight?.value || 0),
|
||
0
|
||
),
|
||
},
|
||
'origin-postal-code': details.origin.address.postal_code.replace(
|
||
/\s/g,
|
||
''
|
||
),
|
||
destination: {
|
||
domestic: {
|
||
'postal-code': details.destination.address.postal_code.replace(
|
||
/\s/g,
|
||
''
|
||
),
|
||
},
|
||
},
|
||
};
|
||
const canadaPostRates = await this.canadaPostService.getRates(rateRequest);
|
||
|
||
await sleep(3000);
|
||
const rates = await this.freightcomService.getRates(request_id);
|
||
return [...rates, ...canadaPostRates];
|
||
}
|
||
|
||
async getShipmentLabel(shipmentId) {
|
||
try {
|
||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
|
||
if (!shipment) {
|
||
throw new Error('运单不存在');
|
||
}
|
||
return await this.uniExpressService.getLabel(shipment.return_tracking_number);
|
||
} catch (e) {
|
||
throw new Error('获取运单失败');
|
||
}
|
||
}
|
||
|
||
async removeShipment(shipmentId: number) {
|
||
try {
|
||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
|
||
if (shipment.state !== '190') { // todo,写常数
|
||
throw new Error('订单当前状态无法删除');
|
||
}
|
||
const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id });
|
||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||
let transactionError = undefined;
|
||
await dataSource.transaction(async manager => {
|
||
const orderRepo = manager.getRepository(Order);
|
||
const shipmentRepo = manager.getRepository(Shipment);
|
||
|
||
order.shipmentId = null;
|
||
orderRepo.save(order);
|
||
|
||
shipmentRepo.remove(shipment);
|
||
|
||
const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
|
||
console.log('res', res.data); // todo
|
||
|
||
await orderRepo.save(order);
|
||
|
||
}).catch(error => {
|
||
transactionError = error;
|
||
});
|
||
|
||
if (transactionError !== undefined) {
|
||
throw new Error(`数据库同步错误: ${transactionError.message}`);
|
||
}
|
||
|
||
try {
|
||
// 同步订单状态到woocommerce
|
||
const site = await this.siteService.get(Number(order.siteId), true);
|
||
if (order.status === OrderStatus.COMPLETED) {
|
||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||
status: OrderStatus.PROCESSING,
|
||
});
|
||
order.status = OrderStatus.PROCESSING;
|
||
}
|
||
order.orderStatus = ErpOrderStatus.PROCESSING;
|
||
this.orderModel.save(order);
|
||
|
||
// todo 同步到wooccommerce删除运单信息
|
||
await this.wpService.deleteFulfillment(site, order.externalOrderId, shipment.tracking_id);
|
||
} catch (error) {
|
||
console.log('同步到woocommerce失败', error);
|
||
return true;
|
||
}
|
||
|
||
return true;
|
||
} catch {
|
||
throw new Error('删除运单失败');
|
||
}
|
||
}
|
||
|
||
async getShipmentFee(data: ShipmentFeeBookDTO) {
|
||
try {
|
||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||
const reqBody = {
|
||
...convertKeysFromCamelToSnake(data),
|
||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||
currency: 'CAD',
|
||
// item_description: data.sales, // todo: 货品信息
|
||
}
|
||
let resShipmentFee: any;
|
||
if (data.shipmentPlatform === 'uniuni') {
|
||
resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||
} else if (data.shipmentPlatform === 'freightwaves') {
|
||
|
||
|
||
// resShipmentFee = await this.freightwavesService.rateTry(reqBody);
|
||
} else {
|
||
throw new Error('不支持的运单平台');
|
||
}
|
||
|
||
|
||
if (resShipmentFee.status !== 'SUCCESS') {
|
||
throw new Error(resShipmentFee.ret_msg);
|
||
}
|
||
return resShipmentFee.data.totalAfterTax * 100;
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
async createShipment(orderId: number, data: ShipmentBookDTO, userId: number) {
|
||
const order = await this.orderModel.findOneBy({ id: orderId });
|
||
const { sales } = data;
|
||
if (!order) {
|
||
throw new Error('订单不存在');
|
||
}
|
||
if (
|
||
order.orderStatus !== ErpOrderStatus.PROCESSING &&
|
||
order.orderStatus !== ErpOrderStatus.PENDING_RESHIPMENT
|
||
) {
|
||
throw new Error('订单状态不正确 ');
|
||
}
|
||
|
||
let resShipmentOrder;
|
||
try {
|
||
//const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||
// const reqBody = {
|
||
// sender: data.details.origin.contact_name,
|
||
// start_phone: data.details.origin.phone_number,
|
||
// start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||
// pickup_address: data.details.origin.address.address_line_1,
|
||
// pickup_warehouse: stock_point.upStreamStockPointId,
|
||
// shipper_country_code: data.details.origin.address.country,
|
||
// receiver: data.details.destination.contact_name,
|
||
// city: data.details.destination.address.city,
|
||
// province: data.details.destination.address.region,
|
||
// country: data.details.destination.address.country,
|
||
// postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||
// delivery_address: data.details.destination.address.address_line_1,
|
||
// receiver_phone: data.details.destination.phone_number.number,
|
||
// receiver_email: data.details.destination.email_addresses,
|
||
// // item_description: data.sales, // todo: 货品信息
|
||
// length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||
// width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||
// height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||
// dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||
// weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||
// weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||
// currency: 'CAD',
|
||
// custom_field: {
|
||
// 'order_id': order.externalOrderId
|
||
// }
|
||
// }
|
||
|
||
resShipmentOrder = await this.mepShipment(data, order);
|
||
|
||
// if (data.shipmentPlatform === 'uniuni') {
|
||
// // 添加运单
|
||
// resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||
// }
|
||
|
||
// if (data.shipmentPlatform === 'freightwaves') {
|
||
// // 添加运单
|
||
// resShipmentOrder = await this.freightcomService.createShipment(reqBody);
|
||
// }
|
||
|
||
// 记录物流信息,并将订单状态转到完成
|
||
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') {
|
||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||
} else {
|
||
throw new Error('运单生成失败');
|
||
}
|
||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||
let transactionError = undefined;
|
||
let shipmentId = undefined;
|
||
await dataSource.transaction(async manager => {
|
||
const orderRepo = manager.getRepository(Order);
|
||
const shipmentRepo = manager.getRepository(Shipment);
|
||
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数
|
||
|
||
// 同步物流信息到woocommerce
|
||
const site = await this.siteService.get(Number(order.siteId), true);
|
||
let co: any;
|
||
let unique_id: any;
|
||
let state: any;
|
||
if (data.shipmentPlatform === 'uniuni') {
|
||
co = resShipmentOrder.data.tno;
|
||
unique_id = resShipmentOrder.data.uni_order_sn;
|
||
state = resShipmentOrder.data.uni_status_code;
|
||
} else {
|
||
co = resShipmentOrder.data?.shipOrderId;
|
||
unique_id = resShipmentOrder.data?.shipOrderId;
|
||
state = ErpOrderStatus.COMPLETED;
|
||
}
|
||
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||
tracking_number: co,
|
||
tracking_provider: tracking_provider,
|
||
});
|
||
|
||
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
|
||
const shipment = await shipmentRepo.save({
|
||
tracking_provider: tracking_provider,
|
||
tracking_id: res.data.tracking_id,
|
||
unique_id: unique_id,
|
||
stockPointId: String(data.stockPointId), // todo
|
||
state: state,
|
||
return_tracking_number: co,
|
||
fee: data.details.shipmentFee,
|
||
order: order
|
||
});
|
||
order.shipmentId = shipment.id;
|
||
shipmentId = shipment.id;
|
||
}
|
||
|
||
// 同步订单状态到woocommerce
|
||
if (order.source_type != "shopyy") {
|
||
if (order.status !== OrderStatus.COMPLETED) {
|
||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||
status: OrderStatus.COMPLETED,
|
||
});
|
||
order.status = OrderStatus.COMPLETED;
|
||
}
|
||
}
|
||
|
||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||
|
||
await orderRepo.save(order);
|
||
}).catch(error => {
|
||
transactionError = error
|
||
});
|
||
|
||
if (transactionError !== undefined) {
|
||
console.log('err', transactionError);
|
||
throw transactionError;
|
||
}
|
||
|
||
// 更新产品发货信息
|
||
this.orderService.updateOrderSales(order.id, sales);
|
||
|
||
return {
|
||
data: {
|
||
shipmentId
|
||
}
|
||
};
|
||
} catch (error) {
|
||
if (resShipmentOrder.status === 'SUCCESS') {
|
||
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno);
|
||
}
|
||
throw new Error(`上游请求错误:${error}`);
|
||
}
|
||
}
|
||
|
||
async syncShipment() {
|
||
try {
|
||
const shipments = await this.shipmentModel.find({
|
||
where: {
|
||
primary_tracking_number: IsNull(),
|
||
},
|
||
});
|
||
if (!shipments) return;
|
||
for (const shipment of shipments) {
|
||
await this.getShipment(shipment.id);
|
||
}
|
||
} catch (error) {
|
||
console.log('syncShipment', error);
|
||
}
|
||
}
|
||
|
||
async syncShipmentStatus() {
|
||
const shipments = await this.shipmentModel.find({
|
||
where: {
|
||
state: In([
|
||
'draft',
|
||
'waiting-for-transit',
|
||
'waiting-for-scheduling',
|
||
'in-transit',
|
||
]),
|
||
},
|
||
});
|
||
if (!shipments) return;
|
||
for (const item of shipments) {
|
||
try {
|
||
let res;
|
||
if (item.type === ShipmentType.FREIGHTCOM) {
|
||
res = await this.freightcomService.getShipment(item.id);
|
||
} else if (item.type === ShipmentType.CANADAPOST) {
|
||
res = await this.canadaPostService.getShipment(
|
||
item.primary_tracking_number
|
||
);
|
||
res.shipment.id = item.id;
|
||
}
|
||
if (!res) return;
|
||
const shipment = plainToClass(Shipment, res.shipment);
|
||
this.shipmentModel.save(shipment);
|
||
} catch (error) {
|
||
console.log('syncShipmentStatus error');
|
||
}
|
||
}
|
||
}
|
||
|
||
async getShipment(id: number) {
|
||
const orderShipments: OrderShipment[] = await this.orderShipmentModel.find({
|
||
where: { shipment_id: id },
|
||
});
|
||
if (!orderShipments || orderShipments.length === 0) return;
|
||
const oldShipment = await this.shipmentModel.findOneBy({ id });
|
||
let res;
|
||
if (oldShipment.type === ShipmentType.FREIGHTCOM) {
|
||
res = await this.freightcomService.getShipment(id);
|
||
} else if (oldShipment.type === ShipmentType.CANADAPOST) {
|
||
res = await this.canadaPostService.getShipment(
|
||
oldShipment.primary_tracking_number
|
||
);
|
||
res.shipment.id = oldShipment.id;
|
||
}
|
||
if (!res) return;
|
||
const shipment = plainToClass(Shipment, res.shipment);
|
||
await this.shipmentModel.save(shipment);
|
||
for (const orderShipment of orderShipments) {
|
||
const order = await this.orderModel.findOneBy({
|
||
id: orderShipment.order_id,
|
||
});
|
||
const site = await this.siteService.get(Number(order.siteId), true);
|
||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||
status: OrderStatus.COMPLETED,
|
||
});
|
||
await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||
tracking_number: shipment.primary_tracking_number,
|
||
tracking_provider: shipment?.rate?.carrier_name,
|
||
});
|
||
}
|
||
}
|
||
|
||
async delShipment(id: number, userId: number) {
|
||
|
||
const shipment = await this.shipmentModel.findOneBy({ id });
|
||
if (!shipment) throw new Error('物流不存在');
|
||
if (shipment.type === ShipmentType.FREIGHTCOM) {
|
||
await this.freightcomService.cancelShipment(shipment.id);
|
||
} else if (shipment.type === ShipmentType.CANADAPOST) {
|
||
await this.canadaPostService.cancelShipment(shipment.id);
|
||
}
|
||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||
return dataSource.transaction(async manager => {
|
||
const shipmentRepo = manager.getRepository(Shipment);
|
||
const shipmentItemRepo = manager.getRepository(ShipmentItem);
|
||
const orderShipmentRepo = manager.getRepository(OrderShipment);
|
||
const stockRecordRepo = manager.getRepository(StockRecord);
|
||
const stockRepo = manager.getRepository(Stock);
|
||
const orderRepo = manager.getRepository(Order);
|
||
const orderShipments = await orderShipmentRepo.findBy({
|
||
shipment_id: id,
|
||
});
|
||
const shipmentItems = await shipmentItemRepo.findBy({ shipment_id: id });
|
||
await shipmentRepo.delete({ id });
|
||
await shipmentItemRepo.delete({ shipment_id: id });
|
||
await orderShipmentRepo.delete({ shipment_id: id });
|
||
for (const item of shipmentItems) {
|
||
const stock = await stockRepo.findOne({
|
||
where: {
|
||
stockPointId: orderShipments[0].stockPointId,
|
||
sku: item.sku,
|
||
},
|
||
});
|
||
stock.quantity += item.quantity;
|
||
await stockRepo.save(stock);
|
||
await stockRecordRepo.save({
|
||
stockPointId: orderShipments[0].stockPointId,
|
||
sku: item.sku,
|
||
operationType: StockRecordOperationType.IN,
|
||
quantityChange: item.quantity,
|
||
operatorId: userId,
|
||
note: `订单${orderShipments.map(v => v.order_id).join(',')} 取消发货`,
|
||
});
|
||
}
|
||
|
||
await orderRepo.update(
|
||
{ id: In(orderShipments.map(v => v.order_id)) },
|
||
{
|
||
orderStatus: ErpOrderStatus.PENDING_RESHIPMENT,
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
async getOrderList(number: string) {
|
||
const orders = await this.orderModel.find({
|
||
where: {
|
||
externalOrderId: Like(`%${number}%`),
|
||
},
|
||
});
|
||
|
||
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询
|
||
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
|
||
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
|
||
const siteMap = new Map(sites.map((s: any) => [s.id, s.name]));
|
||
|
||
return orders.map(order => ({
|
||
...order,
|
||
name: siteMap.get(order.siteId) || '',
|
||
}));
|
||
}
|
||
|
||
async getListByOrderId(id: number) {
|
||
const item = await this.orderItem.find({ where: { orderId: id } });
|
||
const saleItem = await this.orderSaleModel.find({ where: { orderId: id } });
|
||
|
||
return {
|
||
item,
|
||
saleItem
|
||
};
|
||
}
|
||
|
||
async getList(param: Record<string, any>) {
|
||
const {
|
||
pageSize = 10,
|
||
current = 1,
|
||
return_tracking_number,
|
||
stockPointId,
|
||
externalOrderId,
|
||
} = param;
|
||
|
||
const offset = pageSize * (current - 1);
|
||
const values: any[] = [];
|
||
let whereClause = 'WHERE 1=1';
|
||
|
||
if (return_tracking_number) {
|
||
whereClause += ' AND s.return_tracking_number LIKE ?';
|
||
values.push(`%${return_tracking_number}%`);
|
||
}
|
||
|
||
if (stockPointId) {
|
||
whereClause += ' AND sp.id = ?';
|
||
values.push(stockPointId);
|
||
}
|
||
|
||
// todo,增加订单号搜索
|
||
if (externalOrderId) {
|
||
whereClause += ' AND o.externalOrderId = ?';
|
||
values.push(externalOrderId);
|
||
}
|
||
|
||
const sql = `
|
||
SELECT s.*, sp.name, o.externalOrderId, o.siteId
|
||
FROM shipment s
|
||
LEFT JOIN \`order\` o ON s.order_id = o.id
|
||
LEFT JOIN stock_point sp ON s.stock_point_id = sp.id
|
||
${whereClause}
|
||
ORDER BY s.createdAt DESC
|
||
LIMIT ?, ?
|
||
`;
|
||
|
||
values.push(offset, Number(pageSize));
|
||
|
||
const items = await this.serviceModel.query(sql, values);
|
||
// 单独计算总数
|
||
const countSql = `
|
||
SELECT COUNT(DISTINCT s.id) as total
|
||
FROM shipment s
|
||
LEFT JOIN \`order\` o ON s.order_id = o.id
|
||
LEFT JOIN stock_point sp ON s.stock_point_id = sp.id
|
||
${whereClause}
|
||
`;
|
||
|
||
const countResult = await this.serviceModel.query(
|
||
countSql,
|
||
values.slice(0, values.length - 2)
|
||
);
|
||
const total = countResult[0]?.total || 0;
|
||
|
||
return { items, total, current, pageSize };
|
||
}
|
||
|
||
|
||
async mepShipment(data: ShipmentBookDTO, order: Order) {
|
||
try {
|
||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||
let resShipmentOrder;
|
||
|
||
if (data.shipmentPlatform === 'uniuni') {
|
||
const reqBody = {
|
||
sender: data.details.origin.contact_name,
|
||
start_phone: data.details.origin.phone_number,
|
||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||
pickup_address: data.details.origin.address.address_line_1,
|
||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||
shipper_country_code: data.details.origin.address.country,
|
||
receiver: data.details.destination.contact_name,
|
||
city: data.details.destination.address.city,
|
||
province: data.details.destination.address.region,
|
||
country: data.details.destination.address.country,
|
||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||
delivery_address: data.details.destination.address.address_line_1,
|
||
receiver_phone: data.details.destination.phone_number.number,
|
||
receiver_email: data.details.destination.email_addresses,
|
||
// item_description: data.sales, // todo: 货品信息
|
||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||
currency: 'CAD',
|
||
custom_field: {
|
||
'order_id': order.externalOrderId // todo: 需要获取订单的externalOrderId
|
||
}
|
||
};
|
||
|
||
// 添加运单
|
||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||
}
|
||
|
||
if (data.shipmentPlatform === 'freightwaves') {
|
||
// 根据TMS系统对接说明文档格式化参数
|
||
const reqBody: any = {
|
||
shipCompany: 'UPSYYZ7000NEW',
|
||
partnerOrderNumber: order.siteId + '-1-' + order.externalOrderId,
|
||
warehouseId: '25072621030107400060',
|
||
shipper: {
|
||
name: data.details.origin.contact_name, // 姓名
|
||
phone: data.details.origin.phone_number.number, // 电话(提取number属性转换为字符串)
|
||
company: '', // 公司
|
||
countryCode: data.details.origin.address.country, // 国家Code
|
||
city: data.details.origin.address.city, // 城市
|
||
state: data.details.origin.address.region, // 州/省Code,两个字母缩写
|
||
address1: data.details.origin.address.address_line_1, // 详细地址
|
||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||
postCode: data.details.origin.address.postal_code.replace(/\s/g, ''), // 邮编
|
||
countryName: data.details.origin.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||
cityName: data.details.origin.address.city, // 城市名称
|
||
stateName: data.details.origin.address.region, // 州/省名称
|
||
companyName: '' // 公司名称
|
||
},
|
||
reciver: {
|
||
name: data.details.destination.contact_name, // 姓名
|
||
phone: data.details.destination.phone_number.number, // 电话
|
||
company: '', // 公司
|
||
countryCode: data.details.destination.address.country, // 国家Code
|
||
city: data.details.destination.address.city, // 城市
|
||
state: data.details.destination.address.region, // 州/省Code,两个字母的缩写
|
||
address1: data.details.destination.address.address_line_1, // 详细地址
|
||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||
postCode: data.details.destination.address.postal_code.replace(/\s/g, ''), // 邮编
|
||
countryName: data.details.destination.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||
cityName: data.details.destination.address.city, // 城市名称
|
||
stateName: data.details.destination.address.region, // 州/省名称
|
||
companyName: '' // 公司名称
|
||
},
|
||
packages: [
|
||
{
|
||
dimensions: {
|
||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l, // 长
|
||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w, // 宽
|
||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h, // 高
|
||
lengthUnit: (data.details.packaging_properties.packages[0].measurements.cuboid.unit === 'cm' ? 'CM' : 'IN') as 'CM' | 'IN', // 长度单位(IN,CM)
|
||
weight: data.details.packaging_properties.packages[0].measurements.weight.value, // 重量
|
||
weightUnit: (data.details.packaging_properties.packages[0].measurements.weight.unit === 'kg' ? 'KG' : 'LB') as 'KG' | 'LB' // 重量单位(LB,KG)
|
||
},
|
||
currency: 'CAD', // 币种(默认CAD)
|
||
description: 'site:' + order.siteId + ' orderId:' + order.externalOrderId // 包裹描述(确保是字符串类型)
|
||
}
|
||
],
|
||
signService: 0
|
||
// signService: 0, // 签名服务 0不使用, 1使用
|
||
// declaration: {
|
||
// "boxNo": "", //箱子编号
|
||
// "sku": "", //SKU
|
||
// "cnname": "", //中文名称
|
||
// "enname": "", //英文名称
|
||
// "declaredPrice": 1, //申报单价
|
||
// "declaredQty": 1, //申报数量
|
||
// "material": "", //材质
|
||
// "intendedUse": "", //用途
|
||
// "cweight": 1, //产品单重
|
||
// "hsCode": "", //海关编码
|
||
// "battery": "" //电池描述
|
||
// }
|
||
};
|
||
|
||
// 调用freightwaves费用试算或创建订单API
|
||
// 注意:根据实际需要调用对应的方法
|
||
// resShipmentOrder = await this.freightwavesService.rateTry(reqBody); // 费用试算
|
||
resShipmentOrder = await this.freightwavesService.createOrder(reqBody); // 创建订单
|
||
}
|
||
|
||
return resShipmentOrder;
|
||
} catch (error) {
|
||
console.log('物流订单处理失败:', error); // 使用console.log代替this.log
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将ShipmentFeeBookDTO转换为freightwaves的RateTryRequest格式
|
||
* @param data ShipmentFeeBookDTO数据
|
||
* @returns RateTryRequest格式的数据
|
||
*/
|
||
convertToFreightwavesRateTry(data: ShipmentFeeBookDTO): Omit<RateTryRequest, 'partner'> {
|
||
// 转换为RateTryRequest格式
|
||
return {
|
||
shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少
|
||
partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成
|
||
warehouseId: '25072621030107400060', // 可选,使用stockPointId转换
|
||
shipper: {
|
||
name: data.sender, // 必填
|
||
phone: data.startPhone, // 必填
|
||
company: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
countryCode: data.shipperCountryCode, // 必填
|
||
city: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
state: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
address1: data.pickupAddress, // 必填
|
||
address2: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
postCode: data.startPostalCode, // 必填
|
||
countryName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
cityName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
stateName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
companyName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
},
|
||
reciver: {
|
||
name: data.receiver, // 必填
|
||
phone: data.receiverPhone, // 必填
|
||
company: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
countryCode: data.country, // 必填,使用country代替countryCode
|
||
city: data.city, // 必填
|
||
state: data.province, // 必填,使用province代替state
|
||
address1: data.deliveryAddress, // 必填
|
||
address2: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
postCode: data.postalCode, // 必填
|
||
countryName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
cityName: data.city, // 必填,使用city代替cityName
|
||
stateName: data.province, // 必填,使用province代替stateName
|
||
companyName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||
},
|
||
packages: [
|
||
{
|
||
dimensions: {
|
||
length: data.length, // 必填
|
||
width: data.width, // 必填
|
||
height: data.height, // 必填
|
||
lengthUnit: (data.dimensionUom.toUpperCase() === 'CM' ? 'CM' : 'IN') as 'CM' | 'IN', // 必填,转换为有效的单位
|
||
weight: data.weight, // 必填
|
||
weightUnit: (data.weightUom.toUpperCase() === 'KG' ? 'KG' : 'LB') as 'KG' | 'LB', // 必填,转换为有效的单位
|
||
},
|
||
currency: 'CAD', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||
description: 'Package', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||
},
|
||
],
|
||
signService: 0, // 可选,默认不使用签名服务
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取ShipmentFeeBookDTO缺少的freightwaves必填字段
|
||
* @returns 缺少的必填字段列表
|
||
*/
|
||
getMissingFreightwavesFields(): string[] {
|
||
return [
|
||
'shipCompany', // 渠道
|
||
'partnerOrderNumber', // 第三方客户订单编号
|
||
'shipper.company', // 发货人公司
|
||
'shipper.city', // 发货人城市
|
||
'shipper.state', // 发货人州/省Code
|
||
'shipper.address2', // 发货人详细地址2
|
||
'shipper.countryName', // 发货人国家名称
|
||
'shipper.cityName', // 发货人城市名称
|
||
'shipper.stateName', // 发货人州/省名称
|
||
'shipper.companyName', // 发货人公司名称
|
||
'reciver.company', // 收货人公司
|
||
'reciver.address2', // 收货人详细地址2
|
||
'reciver.countryName', // 收货人国家名称
|
||
'reciver.companyName', // 收货人公司名称
|
||
'packages[0].currency', // 包裹币种
|
||
'packages[0].description', // 包裹描述
|
||
'partner', // 商户ID
|
||
];
|
||
}
|
||
}
|