|
|
|
@@ -51,7 +51,10 @@ export class TradeService {
|
|
|
|
|
private readonly fulfillmentService: FulfillmentService,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) {
|
|
|
|
|
async preview(
|
|
|
|
|
userId: bigint,
|
|
|
|
|
body: { productId: string; quantity: number; addressId?: string; onSitePickup?: boolean },
|
|
|
|
|
) {
|
|
|
|
|
const product = await this.catalogService.getProduct(BigInt(body.productId));
|
|
|
|
|
if (!product || product.status !== 'ON_SALE') {
|
|
|
|
|
throw new BadRequestException('商品不可购买');
|
|
|
|
@@ -59,8 +62,15 @@ export class TradeService {
|
|
|
|
|
const city = await this.prisma.commonCity.findFirst({ where: { status: 'ACTIVE' } });
|
|
|
|
|
if (!city) throw new BadRequestException('暂无开城城市');
|
|
|
|
|
|
|
|
|
|
let deliveryType: 'LOCAL' | 'CROSS_CITY' = 'LOCAL';
|
|
|
|
|
if (body.addressId) {
|
|
|
|
|
const onSitePickup = !!body.onSitePickup;
|
|
|
|
|
if (onSitePickup && !product.allowOnSitePickup) {
|
|
|
|
|
throw new BadRequestException('该商品不支持现场取货');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP' = onSitePickup
|
|
|
|
|
? 'ON_SITE_PICKUP'
|
|
|
|
|
: 'LOCAL';
|
|
|
|
|
if (!onSitePickup && body.addressId) {
|
|
|
|
|
const address = await this.prisma.userAddress.findFirst({
|
|
|
|
|
where: { id: BigInt(body.addressId), userId },
|
|
|
|
|
});
|
|
|
|
@@ -84,13 +94,19 @@ export class TradeService {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const freightPayType: FreightPayType | null = deliveryType === 'CROSS_CITY' ? 'COD' : null;
|
|
|
|
|
const minQty =
|
|
|
|
|
deliveryType === 'ON_SITE_PICKUP'
|
|
|
|
|
? 1
|
|
|
|
|
: deliveryType === 'LOCAL'
|
|
|
|
|
? city.localMinQty
|
|
|
|
|
: city.crossMinQty;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
product,
|
|
|
|
|
quantity: body.quantity,
|
|
|
|
|
deliveryType,
|
|
|
|
|
productAmount,
|
|
|
|
|
freightAmount: deliveryType === 'CROSS_CITY' ? 0 : 0,
|
|
|
|
|
freightAmount: 0,
|
|
|
|
|
freightPayType,
|
|
|
|
|
payAmount: productAmount,
|
|
|
|
|
benefitAmount: benefitPerUnit * body.quantity,
|
|
|
|
@@ -98,7 +114,8 @@ export class TradeService {
|
|
|
|
|
/** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */
|
|
|
|
|
quantityOk: check.ok,
|
|
|
|
|
quantityMessage: check.ok ? null : (check.message ?? null),
|
|
|
|
|
minQty: deliveryType === 'LOCAL' ? city.localMinQty : city.crossMinQty,
|
|
|
|
|
minQty,
|
|
|
|
|
onSitePickup,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -107,7 +124,8 @@ export class TradeService {
|
|
|
|
|
body: {
|
|
|
|
|
productId: string;
|
|
|
|
|
quantity: number;
|
|
|
|
|
addressId: string;
|
|
|
|
|
addressId?: string;
|
|
|
|
|
onSitePickup?: boolean;
|
|
|
|
|
clientLocation?: unknown;
|
|
|
|
|
},
|
|
|
|
|
req: Request,
|
|
|
|
@@ -116,10 +134,35 @@ export class TradeService {
|
|
|
|
|
if (preview.quantityOk === false) {
|
|
|
|
|
throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求');
|
|
|
|
|
}
|
|
|
|
|
const address = await this.prisma.userAddress.findFirst({
|
|
|
|
|
where: { id: BigInt(body.addressId), userId },
|
|
|
|
|
});
|
|
|
|
|
if (!address) throw new BadRequestException('请选择收货地址');
|
|
|
|
|
|
|
|
|
|
const onSitePickup = !!body.onSitePickup || preview.deliveryType === 'ON_SITE_PICKUP';
|
|
|
|
|
let receiverName = '现场取货';
|
|
|
|
|
let receiverPhone = '00000000000';
|
|
|
|
|
let receiverAddress = '现场取货';
|
|
|
|
|
let receiverProvince = '';
|
|
|
|
|
let receiverCity = '';
|
|
|
|
|
let receiverDistrict = '';
|
|
|
|
|
|
|
|
|
|
if (onSitePickup) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
|
|
|
receiverPhone = user?.phone || '00000000000';
|
|
|
|
|
receiverName = (user?.nickname?.trim() || '现场取货').slice(0, 32);
|
|
|
|
|
receiverProvince = '现场';
|
|
|
|
|
receiverCity = '现场';
|
|
|
|
|
receiverDistrict = '取货';
|
|
|
|
|
} else {
|
|
|
|
|
if (!body.addressId) throw new BadRequestException('请选择收货地址');
|
|
|
|
|
const address = await this.prisma.userAddress.findFirst({
|
|
|
|
|
where: { id: BigInt(body.addressId), userId },
|
|
|
|
|
});
|
|
|
|
|
if (!address) throw new BadRequestException('请选择收货地址');
|
|
|
|
|
receiverName = address.receiverName;
|
|
|
|
|
receiverPhone = address.phone;
|
|
|
|
|
receiverAddress = `${address.province}${address.city}${address.district}${address.detail}`;
|
|
|
|
|
receiverProvince = address.province;
|
|
|
|
|
receiverCity = address.city;
|
|
|
|
|
receiverDistrict = address.district;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
|
|
|
|
|
where: { id: BigInt(body.productId) },
|
|
|
|
@@ -149,7 +192,7 @@ export class TradeService {
|
|
|
|
|
cityId: city.id,
|
|
|
|
|
status: 'PENDING_PAY',
|
|
|
|
|
payStatus: 'UNPAID',
|
|
|
|
|
deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY',
|
|
|
|
|
deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP',
|
|
|
|
|
productId: product.id,
|
|
|
|
|
barcode69: product.barcode69,
|
|
|
|
|
productName: product.name,
|
|
|
|
@@ -159,12 +202,12 @@ export class TradeService {
|
|
|
|
|
listUnitPrice: product.price,
|
|
|
|
|
listAmount: preview.productAmount,
|
|
|
|
|
productAmount: preview.productAmount,
|
|
|
|
|
receiverName: address.receiverName,
|
|
|
|
|
receiverPhone: address.phone,
|
|
|
|
|
receiverAddress: `${address.province}${address.city}${address.district}${address.detail}`,
|
|
|
|
|
receiverProvince: address.province,
|
|
|
|
|
receiverCity: address.city,
|
|
|
|
|
receiverDistrict: address.district,
|
|
|
|
|
receiverName,
|
|
|
|
|
receiverPhone,
|
|
|
|
|
receiverAddress,
|
|
|
|
|
receiverProvince,
|
|
|
|
|
receiverCity,
|
|
|
|
|
receiverDistrict,
|
|
|
|
|
clientIp: location.clientIp,
|
|
|
|
|
ipProvince: location.ipProvince,
|
|
|
|
|
ipCity: location.ipCity,
|
|
|
|
@@ -203,6 +246,7 @@ export class TradeService {
|
|
|
|
|
orderId: order.id.toString(),
|
|
|
|
|
productId: body.productId,
|
|
|
|
|
quantity: body.quantity,
|
|
|
|
|
onSitePickup,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -241,12 +285,14 @@ export class TradeService {
|
|
|
|
|
order.cityId,
|
|
|
|
|
order.receiverDistrict,
|
|
|
|
|
);
|
|
|
|
|
const toStatus =
|
|
|
|
|
order.deliveryType === 'ON_SITE_PICKUP' ? 'PENDING_RECEIVE' : 'PENDING_SHIP';
|
|
|
|
|
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
await tx.order.update({
|
|
|
|
|
where: { id: order.id },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'PENDING_SHIP',
|
|
|
|
|
status: toStatus,
|
|
|
|
|
payStatus: 'PAID',
|
|
|
|
|
paidAt: now,
|
|
|
|
|
payExternalNo: externalNo,
|
|
|
|
@@ -269,7 +315,7 @@ export class TradeService {
|
|
|
|
|
data: buildOrderStatusEvent({
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
fromStatus: 'PENDING_PAY',
|
|
|
|
|
toStatus: 'PENDING_SHIP',
|
|
|
|
|
toStatus,
|
|
|
|
|
operator: 'MOCK_PAY',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
@@ -292,6 +338,20 @@ export class TradeService {
|
|
|
|
|
|
|
|
|
|
private async afterOrderPaid(orderId: bigint) {
|
|
|
|
|
await this.benefitService.grantOnOrderPaid(orderId);
|
|
|
|
|
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
|
|
|
|
|
if (!order) return;
|
|
|
|
|
|
|
|
|
|
const delivery = await this.prisma.orderDelivery.findUnique({ where: { orderId } });
|
|
|
|
|
if (!delivery) {
|
|
|
|
|
await this.prisma.orderDelivery.create({
|
|
|
|
|
data: { orderId, provider: 'MANUAL' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (order.deliveryType === 'ON_SITE_PICKUP') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.fulfillmentService.dispatchAfterPay(orderId);
|
|
|
|
|
const refreshed = await this.prisma.order.findUnique({ where: { id: orderId } });
|
|
|
|
|
if (refreshed?.status === 'PENDING_SHIP') {
|
|
|
|
@@ -335,6 +395,8 @@ export class TradeService {
|
|
|
|
|
order.cityId,
|
|
|
|
|
order.receiverDistrict,
|
|
|
|
|
);
|
|
|
|
|
const toStatus =
|
|
|
|
|
order.deliveryType === 'ON_SITE_PICKUP' ? 'PENDING_RECEIVE' : 'PENDING_SHIP';
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
const current = await tx.order.findUnique({ where: { id: order.id } });
|
|
|
|
|
if (!current || current.payStatus === 'PAID') return;
|
|
|
|
@@ -342,7 +404,7 @@ export class TradeService {
|
|
|
|
|
await tx.order.update({
|
|
|
|
|
where: { id: order.id },
|
|
|
|
|
data: {
|
|
|
|
|
status: 'PENDING_SHIP',
|
|
|
|
|
status: toStatus,
|
|
|
|
|
payStatus: 'PAID',
|
|
|
|
|
paidAt: now,
|
|
|
|
|
payExternalNo: params.transactionId,
|
|
|
|
@@ -365,7 +427,7 @@ export class TradeService {
|
|
|
|
|
data: buildOrderStatusEvent({
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
fromStatus: 'PENDING_PAY',
|
|
|
|
|
toStatus: 'PENDING_SHIP',
|
|
|
|
|
toStatus,
|
|
|
|
|
operator: 'WECHAT_PAY',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
@@ -470,25 +532,12 @@ export class TradeService {
|
|
|
|
|
async confirmReceive(
|
|
|
|
|
userId: bigint,
|
|
|
|
|
orderId: bigint,
|
|
|
|
|
opts?: { onSitePickup?: boolean },
|
|
|
|
|
_opts?: { onSitePickup?: boolean },
|
|
|
|
|
) {
|
|
|
|
|
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
|
|
|
|
if (!order) throw new NotFoundException('订单不存在');
|
|
|
|
|
|
|
|
|
|
const onSitePickup = !!opts?.onSitePickup;
|
|
|
|
|
const onSiteEligible = [
|
|
|
|
|
'PENDING_SHIP',
|
|
|
|
|
'OUT_WAREHOUSE',
|
|
|
|
|
'SHIPPING',
|
|
|
|
|
'SHIPPED',
|
|
|
|
|
'PENDING_RECEIVE',
|
|
|
|
|
'DELIVERED',
|
|
|
|
|
];
|
|
|
|
|
if (onSitePickup) {
|
|
|
|
|
if (!onSiteEligible.includes(order.status)) {
|
|
|
|
|
throw new BadRequestException('当前状态不可现场取货');
|
|
|
|
|
}
|
|
|
|
|
} else if (!['PENDING_RECEIVE', 'DELIVERED'].includes(order.status)) {
|
|
|
|
|
if (!['PENDING_RECEIVE', 'DELIVERED'].includes(order.status)) {
|
|
|
|
|
throw new BadRequestException('当前状态不可确认收货');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -496,8 +545,8 @@ export class TradeService {
|
|
|
|
|
order.id,
|
|
|
|
|
order.status,
|
|
|
|
|
'COMPLETED',
|
|
|
|
|
onSitePickup ? 'USER_ON_SITE' : 'USER',
|
|
|
|
|
onSitePickup ? '用户现场取货确认收货' : undefined,
|
|
|
|
|
order.deliveryType === 'ON_SITE_PICKUP' ? 'USER_ON_SITE' : 'USER',
|
|
|
|
|
order.deliveryType === 'ON_SITE_PICKUP' ? '用户现场取货确认收货' : undefined,
|
|
|
|
|
);
|
|
|
|
|
return this.getOrder(userId, orderId);
|
|
|
|
|
}
|
|
|
|
|