fix;提交、
This commit is contained in:
@@ -143,7 +143,7 @@ export class CreateEventDto {
|
||||
|
||||
export class CreateTicketDto {
|
||||
@IsString()
|
||||
@IsIn(['REFUND', 'RESHIPMENT', 'ALERT'])
|
||||
@IsIn(['REFUND', 'RESHIPMENT', 'ALERT', 'DAMAGE_RETURN', 'RETURN_REFUND'])
|
||||
ticketType: string;
|
||||
|
||||
@IsString()
|
||||
@@ -165,6 +165,9 @@ export class CreateTicketDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
param1Desc?: string;
|
||||
|
||||
@IsOptional()
|
||||
extraJson?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class UpdateTicketStatusDto {
|
||||
|
||||
@@ -10,7 +10,9 @@ import { AssignTicketDto, CreateTicketDto, UpdateTicketStatusDto } from './dto/c
|
||||
export class TicketController {
|
||||
constructor(private readonly service: TicketService) {}
|
||||
|
||||
/** 总部建单;用户售后请走 /trade/orders/:id/after-sale-tickets */
|
||||
@Post()
|
||||
@UseGuards(HqAuthGuard)
|
||||
create(@Body() dto: CreateTicketDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
@@ -26,6 +28,7 @@ export class TicketController {
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
@UseGuards(HqAuthGuard)
|
||||
updateStatus(@Param('id') id: string, @Body() dto: UpdateTicketStatusDto) {
|
||||
return this.service.updateStatus(BigInt(id), dto);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export class TicketService {
|
||||
remark: dto.remark,
|
||||
param1: dto.param1,
|
||||
param1Desc: dto.param1Desc,
|
||||
extraJson: dto.extraJson ? (dto.extraJson as Prisma.InputJsonValue) : undefined,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(ticket);
|
||||
@@ -69,6 +70,24 @@ export class TicketService {
|
||||
return serializeBigInt(ticket);
|
||||
}
|
||||
|
||||
async updateExtraJson(id: bigint, extraJson: Record<string, unknown>, status?: string, remark?: string) {
|
||||
await this.detail(id);
|
||||
const ticket = await this.prisma.commonTicket.update({
|
||||
where: { id },
|
||||
data: {
|
||||
extraJson: extraJson as Prisma.InputJsonValue,
|
||||
...(status
|
||||
? {
|
||||
status,
|
||||
completedAt: ['COMPLETED', 'CLOSED', 'RESOLVED'].includes(status) ? new Date() : undefined,
|
||||
}
|
||||
: {}),
|
||||
...(remark !== undefined ? { remark } : {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(ticket);
|
||||
}
|
||||
|
||||
async assign(id: bigint, dto: AssignTicketDto) {
|
||||
await this.detail(id);
|
||||
const ticket = await this.prisma.commonTicket.update({
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
import { IssueInvoiceDto, RejectInvoiceDto } from '../trade/dto/after-sale.dto';
|
||||
|
||||
@Controller('admin/invoices')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminInvoicesController {
|
||||
constructor(private readonly tradeService: TradeService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@Query('status') status?: string,
|
||||
@Query('page') page = '1',
|
||||
@Query('pageSize') pageSize = '20',
|
||||
) {
|
||||
return this.tradeService.adminListInvoices({
|
||||
status,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.tradeService.adminGetInvoice(BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/issue')
|
||||
issue(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: IssueInvoiceDto,
|
||||
) {
|
||||
return this.tradeService.adminIssueInvoice(BigInt(id), user.actorId, body);
|
||||
}
|
||||
|
||||
@Post(':id/reject')
|
||||
reject(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: RejectInvoiceDto,
|
||||
) {
|
||||
return this.tradeService.adminRejectInvoice(BigInt(id), user.actorId, body.remark);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
||||
import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { AdminTicketsService } from './admin-tickets.service';
|
||||
@@ -27,8 +31,12 @@ export class AdminTicketsController {
|
||||
refIdParam: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
approve(@Param('id') id: string, @Body() body: { remark?: string }) {
|
||||
return this.service.approve(BigInt(id), body.remark);
|
||||
approve(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { remark?: string },
|
||||
) {
|
||||
return this.service.approve(BigInt(id), body.remark, user.actorId.toString());
|
||||
}
|
||||
|
||||
@Post(':id/reject')
|
||||
@@ -41,4 +49,52 @@ export class AdminTicketsController {
|
||||
reject(@Param('id') id: string, @Body() body: { remark?: string }) {
|
||||
return this.service.reject(BigInt(id), body.remark);
|
||||
}
|
||||
|
||||
@Post(':id/complete-collab')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.TICKET_APPROVE,
|
||||
refType: 'TICKET',
|
||||
refIdParam: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
completeCollab(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.service.completeCollabByHq(BigInt(id), user.actorId.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('partner/tickets')
|
||||
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
|
||||
@RequirePartnerPermissions('warehouse:manage', 'order:view')
|
||||
export class PartnerTicketsController {
|
||||
constructor(private readonly service: AdminTicketsService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.service.listPartnerTickets(
|
||||
user.actorId,
|
||||
page ? Number(page) : 1,
|
||||
pageSize ? Number(pageSize) : 20,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.service.getPartnerTicket(user.actorId, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/confirm-pickup')
|
||||
@RequirePartnerPermissions('warehouse:manage')
|
||||
confirmPickup(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.service.partnerConfirmPickup(user.actorId, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/confirm-reship')
|
||||
@RequirePartnerPermissions('warehouse:manage')
|
||||
confirmReship(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.service.partnerConfirmReship(user.actorId, BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { BenefitService } from '../benefit/benefit.service';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
@@ -7,6 +8,20 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { TicketListQueryDto } from '../common/dto/common-query.dto';
|
||||
|
||||
export type CollabPhase = 'AWAITING_PICKUP' | 'AWAITING_RESHIP' | 'HQ_DIRECT';
|
||||
|
||||
export type TicketCollabExtra = {
|
||||
evidenceUrls?: string[];
|
||||
warehouseId?: string;
|
||||
warehouseName?: string;
|
||||
warehousePartnerAccountId?: string;
|
||||
storePartnerAccountId?: string;
|
||||
collabPhase?: CollabPhase;
|
||||
collabLogs?: { at: string; actorType: string; actorId: string; action: string }[];
|
||||
};
|
||||
|
||||
const COLLAB_TYPES = ['RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class AdminTicketsService {
|
||||
constructor(
|
||||
@@ -25,55 +40,161 @@ export class AdminTicketsService {
|
||||
return this.ticketService.detail(id);
|
||||
}
|
||||
|
||||
async approve(id: bigint, remark?: string) {
|
||||
private parseExtra(raw: unknown): TicketCollabExtra {
|
||||
if (!raw || typeof raw !== 'object') return {};
|
||||
return raw as TicketCollabExtra;
|
||||
}
|
||||
|
||||
private appendLog(
|
||||
extra: TicketCollabExtra,
|
||||
actorType: string,
|
||||
actorId: string,
|
||||
action: string,
|
||||
): TicketCollabExtra {
|
||||
const logs = [...(extra.collabLogs ?? [])];
|
||||
logs.push({ at: new Date().toISOString(), actorType, actorId, action });
|
||||
return { ...extra, collabLogs: logs };
|
||||
}
|
||||
|
||||
/** 解析订单负责仓:优先履约仓,否则城内首个 ACTIVE 仓 */
|
||||
async resolveWarehouseForOrder(orderId: bigint) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
select: {
|
||||
id: true,
|
||||
cityId: true,
|
||||
fulfillmentWarehouseId: true,
|
||||
partnerAccountIdAtPay: true,
|
||||
orderNo: true,
|
||||
receiverAddress: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
productName: true,
|
||||
},
|
||||
});
|
||||
if (!order) throw new NotFoundException('关联订单不存在');
|
||||
|
||||
let warehouse = order.fulfillmentWarehouseId
|
||||
? await this.prisma.cityWarehouse.findUnique({ where: { id: order.fulfillmentWarehouseId } })
|
||||
: null;
|
||||
if (!warehouse) {
|
||||
warehouse = await this.prisma.cityWarehouse.findFirst({
|
||||
where: { cityId: order.cityId, status: 'ACTIVE' },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
}
|
||||
return { order, warehouse };
|
||||
}
|
||||
|
||||
private async executeRefund(orderId: bigint, remark: string) {
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'REFUNDED', payStatus: 'REFUNDED' },
|
||||
});
|
||||
await this.benefitService.voidCouponsOnRefund(orderId);
|
||||
await this.prisma.logThirdParty.create({
|
||||
data: {
|
||||
provider: 'WECHAT_REFUND',
|
||||
scene: 'ORDER_REFUND',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
status: 'SUCCESS',
|
||||
amount: 0,
|
||||
},
|
||||
});
|
||||
await this.prisma.commonEvent.create({
|
||||
data: {
|
||||
eventType: 'ORDER_STATUS',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
actorType: 'HQ',
|
||||
status: 'REFUNDED',
|
||||
remark,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async executeReship(orderId: bigint) {
|
||||
await this.tradeService.applyStatusTransition(orderId, 'PENDING_SHIP', 'PENDING_SHIP', 'HQ');
|
||||
}
|
||||
|
||||
async approve(id: bigint, remark?: string, actorId = '0') {
|
||||
const ticket = await this.prisma.commonTicket.findUnique({ where: { id } });
|
||||
if (!ticket) throw new NotFoundException('工单不存在');
|
||||
if (ticket.status !== 'PENDING' && ticket.status !== 'OPEN') {
|
||||
throw new BadRequestException('工单状态不可审批');
|
||||
}
|
||||
|
||||
if (ticket.ticketType === 'REFUND' && ticket.refType === 'ORDER') {
|
||||
const orderId = ticket.refId;
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'REFUNDED', payStatus: 'REFUNDED' },
|
||||
});
|
||||
await this.benefitService.voidCouponsOnRefund(orderId);
|
||||
await this.prisma.logThirdParty.create({
|
||||
data: {
|
||||
provider: 'WECHAT_REFUND',
|
||||
scene: 'ORDER_REFUND',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
status: 'SUCCESS',
|
||||
amount: 0,
|
||||
},
|
||||
});
|
||||
await this.prisma.commonEvent.create({
|
||||
data: {
|
||||
eventType: 'ORDER_STATUS',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
actorType: 'HQ',
|
||||
status: 'REFUNDED',
|
||||
remark: remark ?? '退款工单审批通过',
|
||||
},
|
||||
});
|
||||
if (ticket.refType !== 'ORDER') {
|
||||
throw new BadRequestException('仅支持订单售后工单');
|
||||
}
|
||||
|
||||
if (ticket.ticketType === 'RESHIPMENT' && ticket.refType === 'ORDER') {
|
||||
await this.tradeService.applyStatusTransition(
|
||||
ticket.refId,
|
||||
'PENDING_SHIP',
|
||||
'PENDING_SHIP',
|
||||
'HQ',
|
||||
const existingExtra = this.parseExtra(ticket.extraJson);
|
||||
|
||||
// 仅退款:立即退款
|
||||
if (ticket.ticketType === 'REFUND') {
|
||||
await this.executeRefund(ticket.refId, remark ?? '仅退款工单审批通过');
|
||||
return this.ticketService.updateExtraJson(
|
||||
id,
|
||||
this.appendLog(existingExtra, 'HQ', actorId, 'APPROVE_REFUND'),
|
||||
'RESOLVED',
|
||||
remark ?? '审批通过',
|
||||
);
|
||||
}
|
||||
|
||||
return this.ticketService.updateStatus(id, {
|
||||
status: 'RESOLVED',
|
||||
remark: remark ?? '审批通过',
|
||||
if (!(COLLAB_TYPES as readonly string[]).includes(ticket.ticketType)) {
|
||||
return this.ticketService.updateStatus(id, {
|
||||
status: 'RESOLVED',
|
||||
remark: remark ?? '审批通过',
|
||||
});
|
||||
}
|
||||
|
||||
const { order, warehouse } = await this.resolveWarehouseForOrder(ticket.refId);
|
||||
const warehousePartnerAccountId =
|
||||
warehouse?.partnerAccountId?.toString() ||
|
||||
(warehouse
|
||||
? (
|
||||
await this.prisma.partnerAccount.findFirst({
|
||||
where: { managedWarehouseId: warehouse.id },
|
||||
select: { id: true },
|
||||
})
|
||||
)?.id.toString()
|
||||
: undefined);
|
||||
|
||||
let collabPhase: CollabPhase = warehousePartnerAccountId ? 'AWAITING_PICKUP' : 'HQ_DIRECT';
|
||||
if (ticket.ticketType === 'RESHIPMENT' && warehousePartnerAccountId) {
|
||||
collabPhase = 'AWAITING_RESHIP';
|
||||
}
|
||||
if (!warehouse) {
|
||||
collabPhase = 'HQ_DIRECT';
|
||||
}
|
||||
|
||||
const storePartnerAccountId =
|
||||
ticket.ticketType === 'RETURN_REFUND' && order.partnerAccountIdAtPay
|
||||
? order.partnerAccountIdAtPay.toString()
|
||||
: undefined;
|
||||
|
||||
let extra: TicketCollabExtra = {
|
||||
...existingExtra,
|
||||
warehouseId: warehouse?.id.toString(),
|
||||
warehouseName: warehouse?.name,
|
||||
warehousePartnerAccountId,
|
||||
storePartnerAccountId,
|
||||
collabPhase,
|
||||
};
|
||||
extra = this.appendLog(extra, 'HQ', actorId, `APPROVE_COLLAB:${collabPhase}`);
|
||||
|
||||
await this.prisma.commonEvent.create({
|
||||
data: {
|
||||
eventType: 'TICKET_COLLAB',
|
||||
refType: 'TICKET',
|
||||
refId: id,
|
||||
actorType: 'HQ',
|
||||
status: 'COLLABORATING',
|
||||
remark: remark ?? `工单进入协同 ${collabPhase}`,
|
||||
},
|
||||
});
|
||||
|
||||
return this.ticketService.updateExtraJson(id, extra as Record<string, unknown>, 'COLLABORATING', remark ?? '审批通过,待协同');
|
||||
}
|
||||
|
||||
reject(id: bigint, remark?: string) {
|
||||
@@ -83,21 +204,203 @@ export class AdminTicketsService {
|
||||
});
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(partnerAccountId);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: orderWhere,
|
||||
/** 完成协同:取回确认(退货类)或补发确认 */
|
||||
async completeCollab(
|
||||
id: bigint,
|
||||
opts: {
|
||||
actorType: 'HQ' | 'PARTNER';
|
||||
actorId: string;
|
||||
mode: 'pickup' | 'reship' | 'auto';
|
||||
},
|
||||
) {
|
||||
const ticket = await this.prisma.commonTicket.findUnique({ where: { id } });
|
||||
if (!ticket) throw new NotFoundException('工单不存在');
|
||||
if (ticket.status !== 'COLLABORATING') {
|
||||
throw new BadRequestException('工单不在协同中');
|
||||
}
|
||||
if (ticket.refType !== 'ORDER') throw new BadRequestException('仅支持订单售后工单');
|
||||
|
||||
let extra = this.parseExtra(ticket.extraJson);
|
||||
const phase = extra.collabPhase ?? 'HQ_DIRECT';
|
||||
|
||||
if (opts.actorType === 'PARTNER') {
|
||||
// 仅管仓合伙人可操作协同节点;归属合伙人只读
|
||||
if (extra.warehousePartnerAccountId !== opts.actorId) {
|
||||
throw new BadRequestException('无权操作此工单');
|
||||
}
|
||||
}
|
||||
|
||||
const mode =
|
||||
opts.mode === 'auto'
|
||||
? ticket.ticketType === 'RESHIPMENT'
|
||||
? 'reship'
|
||||
: 'pickup'
|
||||
: opts.mode;
|
||||
|
||||
if (mode === 'reship' && ticket.ticketType !== 'RESHIPMENT') {
|
||||
throw new BadRequestException('当前工单不是补发类型');
|
||||
}
|
||||
if (mode === 'pickup' && !['DAMAGE_RETURN', 'RETURN_REFUND'].includes(ticket.ticketType)) {
|
||||
throw new BadRequestException('当前工单不需要取回确认');
|
||||
}
|
||||
|
||||
if (ticket.ticketType === 'RESHIPMENT') {
|
||||
await this.executeReship(ticket.refId);
|
||||
extra = this.appendLog(extra, opts.actorType, opts.actorId, 'CONFIRM_RESHIP');
|
||||
} else if (ticket.ticketType === 'DAMAGE_RETURN' || ticket.ticketType === 'RETURN_REFUND') {
|
||||
await this.executeRefund(ticket.refId, `${ticket.ticketType} 协同取回后完成退款`);
|
||||
extra = this.appendLog(extra, opts.actorType, opts.actorId, 'CONFIRM_PICKUP_REFUND');
|
||||
} else {
|
||||
throw new BadRequestException('工单类型不支持协同完成');
|
||||
}
|
||||
|
||||
extra = { ...extra, collabPhase: undefined };
|
||||
|
||||
await this.prisma.commonEvent.create({
|
||||
data: {
|
||||
eventType: 'TICKET_COLLAB',
|
||||
refType: 'TICKET',
|
||||
refId: id,
|
||||
actorType: opts.actorType,
|
||||
status: 'RESOLVED',
|
||||
remark: `协同完成 phaseWas=${phase} mode=${mode}`,
|
||||
},
|
||||
});
|
||||
|
||||
return this.ticketService.updateExtraJson(id, extra as Record<string, unknown>, 'RESOLVED', '协同完成');
|
||||
}
|
||||
|
||||
async completeCollabByHq(id: bigint, actorId: string) {
|
||||
return this.completeCollab(id, { actorType: 'HQ', actorId, mode: 'auto' });
|
||||
}
|
||||
|
||||
async listPartnerTickets(partnerAccountId: bigint, page = 1, pageSize = 20) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const pid = primary.id.toString();
|
||||
|
||||
const warehouses = await this.prisma.cityWarehouse.findMany({
|
||||
where: {
|
||||
OR: [{ partnerAccountId: primary.id }, { managedBy: { id: primary.id } }],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
const orderIds = orders.map((o) => o.id);
|
||||
const warehouseIds = new Set(warehouses.map((w) => w.id.toString()));
|
||||
|
||||
const tickets = await this.prisma.commonTicket.findMany({
|
||||
where: {
|
||||
ticketType: 'RESHIPMENT',
|
||||
status: 'COLLABORATING',
|
||||
ticketType: { in: [...COLLAB_TYPES] },
|
||||
refType: 'ORDER',
|
||||
refId: { in: orderIds },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt(tickets);
|
||||
|
||||
const filtered = tickets.filter((t) => {
|
||||
const extra = this.parseExtra(t.extraJson);
|
||||
if (extra.warehousePartnerAccountId === pid) return true;
|
||||
if (extra.storePartnerAccountId === pid) return true;
|
||||
if (extra.warehouseId && warehouseIds.has(extra.warehouseId)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const pageItems = filtered.slice((page - 1) * pageSize, page * pageSize);
|
||||
const orderIds = pageItems.map((t) => t.refId);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { id: { in: orderIds } },
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
productName: true,
|
||||
receiverAddress: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
payAmount: true,
|
||||
},
|
||||
});
|
||||
const orderMap = new Map(orders.map((o) => [o.id.toString(), o]));
|
||||
|
||||
return serializeBigInt({
|
||||
items: pageItems.map((t) => {
|
||||
const order = orderMap.get(t.refId.toString());
|
||||
const extra = this.parseExtra(t.extraJson);
|
||||
return {
|
||||
...t,
|
||||
extraJson: extra,
|
||||
orderNo: order?.orderNo,
|
||||
productName: order?.productName,
|
||||
receiverAddress: order?.receiverAddress,
|
||||
receiverName: order?.receiverName,
|
||||
receiverPhone: order?.receiverPhone,
|
||||
payAmount: order?.payAmount,
|
||||
};
|
||||
}),
|
||||
total: filtered.length,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async getPartnerTicket(partnerAccountId: bigint, ticketId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const pid = primary.id.toString();
|
||||
const ticket = await this.prisma.commonTicket.findUnique({ where: { id: ticketId } });
|
||||
if (!ticket || ticket.refType !== 'ORDER') throw new NotFoundException('工单不存在或无权查看');
|
||||
const extra = this.parseExtra(ticket.extraJson);
|
||||
const warehouses = await this.prisma.cityWarehouse.findMany({
|
||||
where: {
|
||||
OR: [{ partnerAccountId: primary.id }, { managedBy: { id: primary.id } }],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
const warehouseIds = new Set(warehouses.map((w) => w.id.toString()));
|
||||
const allowed =
|
||||
extra.warehousePartnerAccountId === pid ||
|
||||
extra.storePartnerAccountId === pid ||
|
||||
(extra.warehouseId != null && warehouseIds.has(extra.warehouseId));
|
||||
if (!allowed) throw new NotFoundException('工单不存在或无权查看');
|
||||
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: ticket.refId },
|
||||
select: {
|
||||
orderNo: true,
|
||||
productName: true,
|
||||
receiverAddress: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
payAmount: true,
|
||||
},
|
||||
});
|
||||
return serializeBigInt({
|
||||
...ticket,
|
||||
extraJson: extra,
|
||||
orderNo: order?.orderNo,
|
||||
productName: order?.productName,
|
||||
receiverAddress: order?.receiverAddress,
|
||||
receiverName: order?.receiverName,
|
||||
receiverPhone: order?.receiverPhone,
|
||||
payAmount: order?.payAmount,
|
||||
});
|
||||
}
|
||||
|
||||
async partnerConfirmPickup(partnerAccountId: bigint, ticketId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
return this.completeCollab(ticketId, {
|
||||
actorType: 'PARTNER',
|
||||
actorId: primary.id.toString(),
|
||||
mode: 'pickup',
|
||||
});
|
||||
}
|
||||
|
||||
async partnerConfirmReship(partnerAccountId: bigint, ticketId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
return this.completeCollab(ticketId, {
|
||||
actorType: 'PARTNER',
|
||||
actorId: primary.id.toString(),
|
||||
mode: 'reship',
|
||||
});
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
return this.listPartnerTickets(partnerAccountId, 1, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,9 @@ import { AdminHqLogsController } from './admin-hq-logs.controller';
|
||||
import { AdminHqLogsService } from './admin-hq-logs.service';
|
||||
import { AdminOssLogsController } from './admin-oss-logs.controller';
|
||||
import { AdminOssLogsService } from './admin-oss-logs.service';
|
||||
import { AdminTicketsController } from './admin-tickets.controller';
|
||||
import { AdminTicketsController, PartnerTicketsController } from './admin-tickets.controller';
|
||||
import { AdminTicketsService } from './admin-tickets.service';
|
||||
import { AdminInvoicesController } from './admin-invoices.controller';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
import { BenefitModule } from '../benefit/benefit.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
@@ -84,6 +85,7 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
|
||||
AdminHqLogsController,
|
||||
AdminOssLogsController,
|
||||
AdminTicketsController,
|
||||
AdminInvoicesController,
|
||||
AdminXiaofeixiaController,
|
||||
AdminProductDetailTemplatesController,
|
||||
AdminRedeemDebugController,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateAfterSaleTicketDto {
|
||||
@IsString()
|
||||
@IsIn(['REFUND', 'RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND'])
|
||||
ticketType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
|
||||
@IsOptional()
|
||||
evidenceUrls?: string[];
|
||||
}
|
||||
|
||||
export class CreateInvoiceDto {
|
||||
@IsString()
|
||||
@IsIn(['PERSONAL', 'ENTERPRISE'])
|
||||
titleType: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['NORMAL', 'SPECIAL'])
|
||||
invoiceKind: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(128)
|
||||
titleName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
taxNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
addressPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
bankAccount?: string;
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(20)
|
||||
phone: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export class IssueInvoiceDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
fileUrl: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
resourceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export class RejectInvoiceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PartnerProxyOrderSendSmsDto,
|
||||
} from './dto/partner-proxy-order.dto';
|
||||
import { ManualShipOrderDto } from '../ops/dto/admin-mutate.dto';
|
||||
import { CreateAfterSaleTicketDto, CreateInvoiceDto } from './dto/after-sale.dto';
|
||||
|
||||
@Controller('trade/orders')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -75,6 +76,64 @@ export class TradeController {
|
||||
) {
|
||||
return this.tradeService.createRefundRequest(user.actorId, BigInt(id), body.remark);
|
||||
}
|
||||
|
||||
@Post(':id/after-sale-tickets')
|
||||
createAfterSale(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: CreateAfterSaleTicketDto,
|
||||
) {
|
||||
return this.tradeService.createAfterSaleTicket(user.actorId, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Post(':id/invoices')
|
||||
createInvoice(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: CreateInvoiceDto,
|
||||
) {
|
||||
return this.tradeService.createInvoice(user.actorId, BigInt(id), body);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('trade/after-sale-tickets')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class TradeAfterSaleTicketController {
|
||||
constructor(private readonly tradeService: TradeService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('page') page = '1',
|
||||
@Query('pageSize') pageSize = '20',
|
||||
) {
|
||||
return this.tradeService.listAfterSaleTickets(user.actorId, Number(page), Number(pageSize));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.tradeService.getAfterSaleTicket(user.actorId, BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('trade/invoices')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class TradeInvoiceController {
|
||||
constructor(private readonly tradeService: TradeService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query('page') page = '1',
|
||||
@Query('pageSize') pageSize = '20',
|
||||
) {
|
||||
return this.tradeService.listInvoices(user.actorId, Number(page), Number(pageSize));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.tradeService.getInvoice(user.actorId, BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('partner/orders')
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
PartnerOrderController,
|
||||
PartnerProxyOrderController,
|
||||
PartnerReshipmentController,
|
||||
TradeAfterSaleTicketController,
|
||||
TradeInvoiceController,
|
||||
} from './trade.controller';
|
||||
import { TradeService } from './trade.service';
|
||||
|
||||
@@ -30,6 +32,8 @@ import { TradeService } from './trade.service';
|
||||
],
|
||||
controllers: [
|
||||
TradeController,
|
||||
TradeAfterSaleTicketController,
|
||||
TradeInvoiceController,
|
||||
PartnerOrderController,
|
||||
PartnerProxyOrderController,
|
||||
PartnerReshipmentController,
|
||||
|
||||
@@ -478,23 +478,293 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async createRefundRequest(userId: bigint, orderId: bigint, remark?: string) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (!['PENDING_SHIP', 'PENDING_RECEIVE', 'COMPLETED', 'OUT_WAREHOUSE', 'SHIPPING'].includes(order.status)) {
|
||||
throw new BadRequestException('当前订单不可申请退款');
|
||||
}
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'REFUNDING', payStatus: 'REFUNDING' },
|
||||
});
|
||||
return this.ticketService.create({
|
||||
return this.createAfterSaleTicket(userId, orderId, {
|
||||
ticketType: 'REFUND',
|
||||
refType: 'ORDER',
|
||||
refId: orderId.toString(),
|
||||
remark: remark ?? '用户申请退款',
|
||||
});
|
||||
}
|
||||
|
||||
async createAfterSaleTicket(
|
||||
userId: bigint,
|
||||
orderId: bigint,
|
||||
body: { ticketType: string; remark?: string; evidenceUrls?: string[] },
|
||||
) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (order.status === 'PENDING_PAY' || order.status === 'CANCELLED') {
|
||||
throw new BadRequestException('当前订单不可申请售后');
|
||||
}
|
||||
if (['REFUNDING', 'REFUNDED'].includes(order.status)) {
|
||||
throw new BadRequestException('订单已在退款流程中');
|
||||
}
|
||||
|
||||
const pending = await this.prisma.commonTicket.findFirst({
|
||||
where: {
|
||||
ticketType: body.ticketType as never,
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
status: { in: ['PENDING', 'OPEN'] },
|
||||
},
|
||||
});
|
||||
if (pending) throw new BadRequestException('该类型售后工单已在处理中');
|
||||
|
||||
const evidenceUrls = (body.evidenceUrls ?? []).filter((u) => typeof u === 'string' && u.trim());
|
||||
return this.ticketService.create({
|
||||
ticketType: body.ticketType,
|
||||
refType: 'ORDER',
|
||||
refId: orderId.toString(),
|
||||
remark: body.remark ?? '',
|
||||
extraJson: evidenceUrls.length ? { evidenceUrls } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async listAfterSaleTickets(userId: bigint, page = 1, pageSize = 20) {
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { userId },
|
||||
select: { id: true, orderNo: true },
|
||||
});
|
||||
const orderIds = orders.map((o) => o.id);
|
||||
const orderNoMap = new Map(orders.map((o) => [o.id.toString(), o.orderNo]));
|
||||
if (!orderIds.length) {
|
||||
return serializeBigInt({ items: [], total: 0, page, pageSize });
|
||||
}
|
||||
|
||||
const where = {
|
||||
refType: 'ORDER',
|
||||
refId: { in: orderIds },
|
||||
ticketType: { in: ['REFUND', 'RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND'] as never[] },
|
||||
};
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonTicket.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.commonTicket.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((t) => ({
|
||||
...t,
|
||||
orderNo: orderNoMap.get(t.refId.toString()) ?? null,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async getAfterSaleTicket(userId: bigint, ticketId: bigint) {
|
||||
const ticket = await this.prisma.commonTicket.findUnique({ where: { id: ticketId } });
|
||||
if (!ticket || ticket.refType !== 'ORDER') throw new NotFoundException('工单不存在');
|
||||
const order = await this.prisma.order.findFirst({
|
||||
where: { id: ticket.refId, userId },
|
||||
select: { id: true, orderNo: true, status: true },
|
||||
});
|
||||
if (!order) throw new NotFoundException('工单不存在');
|
||||
return serializeBigInt({ ...ticket, orderNo: order.orderNo, orderStatus: order.status });
|
||||
}
|
||||
|
||||
private generateInvoiceNo() {
|
||||
return `INV${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||
}
|
||||
|
||||
async createInvoice(
|
||||
userId: bigint,
|
||||
orderId: bigint,
|
||||
body: {
|
||||
titleType: string;
|
||||
invoiceKind: string;
|
||||
titleName: string;
|
||||
taxNo?: string;
|
||||
addressPhone?: string;
|
||||
bankAccount?: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
remark?: string;
|
||||
},
|
||||
) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (order.status !== 'COMPLETED') {
|
||||
throw new BadRequestException('仅已完成订单可申请发票');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.userInvoice.findFirst({
|
||||
where: { orderId, status: { in: ['PENDING', 'ISSUED'] } },
|
||||
});
|
||||
if (existing) throw new BadRequestException('该订单已有进行中或已开具的发票申请');
|
||||
|
||||
if (body.titleType === 'ENTERPRISE' && !body.taxNo?.trim()) {
|
||||
throw new BadRequestException('企业抬头须填写税号');
|
||||
}
|
||||
if (body.invoiceKind === 'SPECIAL') {
|
||||
if (body.titleType !== 'ENTERPRISE') {
|
||||
throw new BadRequestException('专用发票仅支持企业抬头');
|
||||
}
|
||||
if (!body.taxNo?.trim() || !body.addressPhone?.trim() || !body.bankAccount?.trim()) {
|
||||
throw new BadRequestException('专用发票须填写税号、地址电话与开户行账号');
|
||||
}
|
||||
}
|
||||
|
||||
const invoice = await this.prisma.userInvoice.create({
|
||||
data: {
|
||||
invoiceNo: this.generateInvoiceNo(),
|
||||
orderId,
|
||||
userId,
|
||||
titleType: body.titleType as never,
|
||||
invoiceKind: body.invoiceKind as never,
|
||||
titleName: body.titleName.trim(),
|
||||
taxNo: body.taxNo?.trim() || null,
|
||||
addressPhone: body.addressPhone?.trim() || null,
|
||||
bankAccount: body.bankAccount?.trim() || null,
|
||||
email: body.email.trim(),
|
||||
phone: body.phone.trim(),
|
||||
remark: body.remark?.trim() || null,
|
||||
},
|
||||
});
|
||||
return serializeBigInt({ ...invoice, orderNo: order.orderNo });
|
||||
}
|
||||
|
||||
async listInvoices(userId: bigint, page = 1, pageSize = 20) {
|
||||
const where = { userId };
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.userInvoice.findMany({
|
||||
where,
|
||||
include: { order: { select: { orderNo: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.userInvoice.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((inv) => ({
|
||||
...inv,
|
||||
orderNo: inv.order.orderNo,
|
||||
order: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async getInvoice(userId: bigint, invoiceId: bigint) {
|
||||
const invoice = await this.prisma.userInvoice.findFirst({
|
||||
where: { id: invoiceId, userId },
|
||||
include: { order: { select: { orderNo: true, payAmount: true } } },
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
return serializeBigInt({
|
||||
...invoice,
|
||||
orderNo: invoice.order.orderNo,
|
||||
payAmount: invoice.order.payAmount,
|
||||
order: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/** 工作日差(粗略:排除周六日) */
|
||||
private businessDaysSince(from: Date, to = new Date()): number {
|
||||
let days = 0;
|
||||
const cur = new Date(from);
|
||||
cur.setHours(0, 0, 0, 0);
|
||||
const end = new Date(to);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
while (cur < end) {
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
const w = cur.getDay();
|
||||
if (w !== 0 && w !== 6) days += 1;
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: { status?: never } = {};
|
||||
if (query.status) where.status = query.status as never;
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.userInvoice.findMany({
|
||||
where,
|
||||
include: { order: { select: { orderNo: true, payAmount: true } }, user: { select: { phone: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.userInvoice.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((inv) => ({
|
||||
...inv,
|
||||
orderNo: inv.order.orderNo,
|
||||
payAmount: inv.order.payAmount,
|
||||
userPhone: inv.user.phone,
|
||||
overdue: inv.status === 'PENDING' && this.businessDaysSince(inv.createdAt) > 2,
|
||||
order: undefined,
|
||||
user: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async adminGetInvoice(id: bigint) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({
|
||||
where: { id },
|
||||
include: { order: { select: { orderNo: true, payAmount: true, productName: true } }, user: { select: { phone: true, nickname: true } } },
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
return serializeBigInt({
|
||||
...invoice,
|
||||
orderNo: invoice.order.orderNo,
|
||||
payAmount: invoice.order.payAmount,
|
||||
productName: invoice.order.productName,
|
||||
userPhone: invoice.user.phone,
|
||||
overdue: invoice.status === 'PENDING' && this.businessDaysSince(invoice.createdAt) > 2,
|
||||
});
|
||||
}
|
||||
|
||||
async adminIssueInvoice(
|
||||
id: bigint,
|
||||
operatorId: bigint,
|
||||
body: { fileUrl: string; resourceId?: string; remark?: string },
|
||||
) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({ where: { id } });
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
if (invoice.status !== 'PENDING') throw new BadRequestException('当前状态不可开票');
|
||||
const updated = await this.prisma.userInvoice.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'ISSUED',
|
||||
fileUrl: body.fileUrl,
|
||||
resourceId: body.resourceId ? BigInt(body.resourceId) : null,
|
||||
issuedAt: new Date(),
|
||||
operatorId,
|
||||
remark: body.remark ?? invoice.remark,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async adminRejectInvoice(id: bigint, operatorId: bigint, remark?: string) {
|
||||
const invoice = await this.prisma.userInvoice.findUnique({ where: { id } });
|
||||
if (!invoice) throw new NotFoundException('发票不存在');
|
||||
if (invoice.status !== 'PENDING') throw new BadRequestException('当前状态不可驳回');
|
||||
const updated = await this.prisma.userInvoice.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
operatorId,
|
||||
remark: remark ?? '驳回',
|
||||
issuedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(updated);
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
|
||||
Reference in New Issue
Block a user