fix: 异常 client_md5 幂等 + checkin/checkout 更新工单状态
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
394d7a9731
commit
c18187e658
@ -0,0 +1,7 @@
|
||||
-- 异常上报离线补传幂等:ho_record_abnormal 增加内容指纹 client_md5 + 唯一索引。
|
||||
-- 唯一索引允许多个 NULL,在线上报(不传 client_md5)不受影响。
|
||||
-- AlterTable
|
||||
ALTER TABLE `ho_record_abnormal` ADD COLUMN `client_md5` VARCHAR(64) NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `ho_record_abnormal_tenant_code_client_md5_key` ON `ho_record_abnormal`(`tenant_code`, `client_md5`);
|
||||
@ -687,9 +687,11 @@ model RecordAbnormal {
|
||||
handleStatus String @default("open") @map("handle_status") @db.VarChar(20)
|
||||
handleResult String? @map("handle_result") @db.VarChar(200)
|
||||
linkPlanChange Boolean @default(false) @map("link_plan_change")
|
||||
clientMd5 String? @map("client_md5") @db.VarChar(64)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String? @map("created_by") @db.VarChar(36)
|
||||
|
||||
@@unique([tenantCode, clientMd5])
|
||||
@@index([tenantCode, elderId])
|
||||
@@index([tenantCode, notifyStatus])
|
||||
@@map("ho_record_abnormal")
|
||||
|
||||
@ -20,6 +20,7 @@ function makeService(
|
||||
recordAbnormalCreate?: ReturnType<typeof vi.fn>;
|
||||
recordAbnormalFindMany?: ReturnType<typeof vi.fn>;
|
||||
recordAbnormalCount?: ReturnType<typeof vi.fn>;
|
||||
recordAbnormalFindFirst?: ReturnType<typeof vi.fn>;
|
||||
orderUpdate?: ReturnType<typeof vi.fn>;
|
||||
orderFindUnique?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
@ -27,6 +28,7 @@ function makeService(
|
||||
const recordAbnormalCreate = overrides.recordAbnormalCreate ?? vi.fn().mockResolvedValue({ id: 'a1' });
|
||||
const recordAbnormalFindMany = overrides.recordAbnormalFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const recordAbnormalCount = overrides.recordAbnormalCount ?? vi.fn().mockResolvedValue(0);
|
||||
const recordAbnormalFindFirst = overrides.recordAbnormalFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||
const orderUpdate = overrides.orderUpdate ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'not_met' });
|
||||
const orderFindUnique = overrides.orderFindUnique ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'assigned' });
|
||||
|
||||
@ -35,6 +37,7 @@ function makeService(
|
||||
create: recordAbnormalCreate,
|
||||
findMany: recordAbnormalFindMany,
|
||||
count: recordAbnormalCount,
|
||||
findFirst: recordAbnormalFindFirst,
|
||||
},
|
||||
order: { update: orderUpdate, findUnique: orderFindUnique },
|
||||
};
|
||||
@ -42,7 +45,15 @@ function makeService(
|
||||
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||
const prisma = { client };
|
||||
const service = new AbnormalService(prisma as unknown as PrismaService);
|
||||
return { service, recordAbnormalCreate, recordAbnormalFindMany, recordAbnormalCount, orderUpdate, orderFindUnique };
|
||||
return {
|
||||
service,
|
||||
recordAbnormalCreate,
|
||||
recordAbnormalFindMany,
|
||||
recordAbnormalCount,
|
||||
recordAbnormalFindFirst,
|
||||
orderUpdate,
|
||||
orderFindUnique,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AbnormalService', () => {
|
||||
@ -72,6 +83,39 @@ describe('AbnormalService', () => {
|
||||
expect(res).toEqual({ id: 'a1', visitLat: null, visitLng: null });
|
||||
});
|
||||
|
||||
it('report 带 clientMd5 命中 → 幂等返回已存在,不重复落库、不转工单', async () => {
|
||||
const recordAbnormalFindFirst = vi.fn().mockResolvedValue({
|
||||
id: 'a-exist',
|
||||
clientMd5: 'md5-1',
|
||||
abnormalType: 'fall',
|
||||
});
|
||||
const { service, recordAbnormalCreate, orderUpdate } = makeService({ recordAbnormalFindFirst });
|
||||
|
||||
const res = await runWithRequestContext(ctx, () =>
|
||||
service.report({ abnormalType: 'fall', elderId: 'e1', clientMd5: 'md5-1' }),
|
||||
);
|
||||
|
||||
expect(recordAbnormalFindFirst).toHaveBeenCalledWith({
|
||||
where: { tenantCode: 'T001', clientMd5: 'md5-1' },
|
||||
});
|
||||
expect(res).toMatchObject({ id: 'a-exist', clientMd5: 'md5-1', visitLat: null, visitLng: null });
|
||||
expect(recordAbnormalCreate).not.toHaveBeenCalled();
|
||||
expect(orderUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('report 带 clientMd5 未命中 → 正常落库并写入 clientMd5', async () => {
|
||||
const recordAbnormalCreate = vi.fn().mockResolvedValue({ id: 'a1', clientMd5: 'md5-2' });
|
||||
const { service } = makeService({ recordAbnormalCreate });
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.report({ abnormalType: 'fall', elderId: 'e1', clientMd5: 'md5-2' }),
|
||||
);
|
||||
|
||||
expect(recordAbnormalCreate).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ clientMd5: 'md5-2' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('report 扑空(not_met)→ 工单转 not_met + 写 record_abnormal(到访时间/定位/原因),不产生 ho_record', async () => {
|
||||
const { service, recordAbnormalCreate, orderUpdate } = makeService();
|
||||
|
||||
|
||||
@ -35,6 +35,16 @@ export class AbnormalService {
|
||||
const reporterId = ctx?.userId ?? '';
|
||||
const createdBy = ctx?.userId ?? null;
|
||||
|
||||
// 幂等:离线补传带 clientMd5,命中即返回已存在记录,超时重试不重复落库(R2)。
|
||||
if (dto.clientMd5) {
|
||||
const existing = await this.prisma.client.recordAbnormal.findFirst({
|
||||
where: { tenantCode, clientMd5: dto.clientMd5 },
|
||||
});
|
||||
if (existing) {
|
||||
return this.serializeAbnormal(existing);
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.abnormalType === NOT_MET) {
|
||||
this.assertNotMetEvidence(dto);
|
||||
// 扑空:工单须存在,再转 not_met(原因 + 到访时间)。
|
||||
@ -71,15 +81,11 @@ export class AbnormalService {
|
||||
visitLng: dto.visitLng ?? null,
|
||||
notifyStatus: 'pending',
|
||||
handleStatus: 'open',
|
||||
clientMd5: dto.clientMd5 ?? null,
|
||||
},
|
||||
});
|
||||
});
|
||||
// Prisma Decimal(visit_lat/visit_lng)转字符串,避免经 TransformInterceptor 泄漏为 {s,e,d}。
|
||||
return {
|
||||
...created,
|
||||
visitLat: created.visitLat != null ? created.visitLat.toString() : null,
|
||||
visitLng: created.visitLng != null ? created.visitLng.toString() : null,
|
||||
};
|
||||
return this.serializeAbnormal(created);
|
||||
}
|
||||
|
||||
/** 异常列表(后台 B4)。 */
|
||||
@ -119,6 +125,15 @@ export class AbnormalService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Prisma Decimal(visit_lat/visit_lng)转字符串,避免经 TransformInterceptor 泄漏为 {s,e,d}。 */
|
||||
private serializeAbnormal(r: Record<string, any>): Record<string, any> {
|
||||
return {
|
||||
...r,
|
||||
visitLat: r.visitLat != null ? r.visitLat.toString() : null,
|
||||
visitLng: r.visitLng != null ? r.visitLng.toString() : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 工单须存在(扑空转 not_met 前校验,44003),循 order.service 的 requireOrder。 */
|
||||
private async requireOrder(id: string): Promise<Record<string, unknown>> {
|
||||
const order = await this.prisma.client.order.findUnique({ where: { id } });
|
||||
|
||||
@ -84,6 +84,11 @@ export class ReportAbnormalDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
visitLng?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '客户端内容指纹(离线补传幂等去重,在线上报可空)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
clientMd5?: string;
|
||||
}
|
||||
|
||||
/** 异常列表查询(后台 B4),按分类/通知状态/处理状态/老人筛选。 */
|
||||
|
||||
@ -19,6 +19,7 @@ const ctx: RequestContext = {
|
||||
function makeService(
|
||||
overrides: {
|
||||
orderFindUnique?: ReturnType<typeof vi.fn>;
|
||||
orderUpdate?: ReturnType<typeof vi.fn>;
|
||||
staffFindFirst?: ReturnType<typeof vi.fn>;
|
||||
elderFindFirst?: ReturnType<typeof vi.fn>;
|
||||
elderFindUnique?: ReturnType<typeof vi.fn>;
|
||||
@ -40,6 +41,7 @@ function makeService(
|
||||
} = {},
|
||||
) {
|
||||
const orderFindUnique = overrides.orderFindUnique ?? vi.fn().mockResolvedValue(null);
|
||||
const orderUpdate = overrides.orderUpdate ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'done' });
|
||||
const staffFindFirst = overrides.staffFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||
const elderFindFirst = overrides.elderFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||
const elderFindUnique = overrides.elderFindUnique ?? vi.fn().mockResolvedValue(null);
|
||||
@ -60,7 +62,7 @@ function makeService(
|
||||
const svcLocalFindFirst = overrides.svcLocalFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||
|
||||
const client: Record<string, any> = {
|
||||
order: { findUnique: orderFindUnique },
|
||||
order: { findUnique: orderFindUnique, update: orderUpdate },
|
||||
staff: { findFirst: staffFindFirst },
|
||||
elder: { findFirst: elderFindFirst, findUnique: elderFindUnique },
|
||||
record: {
|
||||
@ -92,6 +94,7 @@ function makeService(
|
||||
return {
|
||||
service,
|
||||
orderFindUnique,
|
||||
orderUpdate,
|
||||
staffFindFirst,
|
||||
elderFindFirst,
|
||||
elderFindUnique,
|
||||
@ -206,6 +209,31 @@ describe('RecordService.checkin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('签到 assigned 工单 → 同事务把工单转 serving', async () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue(servingOrder);
|
||||
const staffFindFirst = vi.fn().mockResolvedValue({ id: 'staff1', userId: 'u1' });
|
||||
const elderFindFirst = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'active' });
|
||||
const orderUpdate = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'serving' });
|
||||
const { service, recordCreate } = makeService({ orderFindUnique, staffFindFirst, elderFindFirst, orderUpdate });
|
||||
|
||||
await runWithRequestContext(ctx, () => service.checkin({ orderId: 'o1' }));
|
||||
|
||||
expect(orderUpdate).toHaveBeenCalledWith({ where: { id: 'o1' }, data: { orderStatus: 'serving' } });
|
||||
expect(recordCreate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('签到 serving 工单 → 不再重复转 serving(仅建留痕)', async () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue({ ...servingOrder, orderStatus: 'serving' });
|
||||
const staffFindFirst = vi.fn().mockResolvedValue({ id: 'staff1', userId: 'u1' });
|
||||
const elderFindFirst = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'active' });
|
||||
const orderUpdate = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'serving' });
|
||||
const { service } = makeService({ orderFindUnique, staffFindFirst, elderFindFirst, orderUpdate });
|
||||
|
||||
await runWithRequestContext(ctx, () => service.checkin({ orderId: 'o1' }));
|
||||
|
||||
expect(orderUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checkin 返回坐标序列化为字符串(F3)', async () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue(servingOrder);
|
||||
const staffFindFirst = vi.fn().mockResolvedValue({ id: 'staff1', userId: 'u1' });
|
||||
@ -427,6 +455,26 @@ describe('RecordService.checkout', () => {
|
||||
expect(data.durationMin).toBe(30);
|
||||
expect(data.checkoutTime).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('签退 → 同事务把工单转 done', async () => {
|
||||
const recordFindUnique = vi.fn().mockResolvedValue({
|
||||
id: 'r1',
|
||||
orderId: 'o1',
|
||||
recordStatus: 'serving',
|
||||
checkinTime: new Date(Date.now() - 30 * 60 * 1000),
|
||||
});
|
||||
const recordItemFindMany = vi.fn().mockResolvedValue([]);
|
||||
const orderUpdate = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'done' });
|
||||
const { service, recordUpdate } = makeService({ recordFindUnique, recordItemFindMany, orderUpdate });
|
||||
|
||||
await runWithRequestContext(ctx, () => service.checkout({ recordId: 'r1' }));
|
||||
|
||||
expect(orderUpdate).toHaveBeenCalledWith({ where: { id: 'o1' }, data: { orderStatus: 'done' } });
|
||||
expect(recordUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'r1' },
|
||||
data: expect.objectContaining({ recordStatus: 'submitted' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecordService.photoUpload', () => {
|
||||
@ -615,6 +663,57 @@ describe('RecordService.offlineSync', () => {
|
||||
});
|
||||
expect(recordPhotoCreate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('offlineSync 含 checkoutTime → 工单转 done', async () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue({
|
||||
id: 'o1',
|
||||
orderStatus: 'assigned',
|
||||
elderId: 'e1',
|
||||
assigneeId: 'staff1',
|
||||
itemList: [],
|
||||
});
|
||||
const elderFindFirst = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'active' });
|
||||
const orderUpdate = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'done' });
|
||||
const { service } = makeService({ orderFindUnique, elderFindFirst, orderUpdate });
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.offlineSync({
|
||||
clientMd5: 'md5-done',
|
||||
orderId: 'o1',
|
||||
elderId: 'e1',
|
||||
assigneeId: 'staff1',
|
||||
checkinTime: '2026-10-15T02:00:00Z',
|
||||
checkoutTime: '2026-10-15T03:00:00Z',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(orderUpdate).toHaveBeenCalledWith({ where: { id: 'o1' }, data: { orderStatus: 'done' } });
|
||||
});
|
||||
|
||||
it('offlineSync 不含 checkoutTime → 工单转 serving', async () => {
|
||||
const orderFindUnique = vi.fn().mockResolvedValue({
|
||||
id: 'o1',
|
||||
orderStatus: 'assigned',
|
||||
elderId: 'e1',
|
||||
assigneeId: 'staff1',
|
||||
itemList: [],
|
||||
});
|
||||
const elderFindFirst = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'active' });
|
||||
const orderUpdate = vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'serving' });
|
||||
const { service } = makeService({ orderFindUnique, elderFindFirst, orderUpdate });
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.offlineSync({
|
||||
clientMd5: 'md5-serving',
|
||||
orderId: 'o1',
|
||||
elderId: 'e1',
|
||||
assigneeId: 'staff1',
|
||||
checkinTime: '2026-10-15T02:00:00Z',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(orderUpdate).toHaveBeenCalledWith({ where: { id: 'o1' }, data: { orderStatus: 'serving' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecordService.prepaidCarryover', () => {
|
||||
|
||||
@ -66,21 +66,30 @@ export class RecordService {
|
||||
throw new BusinessException(ErrorCodes.ELDER_NOT_FOUND, '老人档案不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
const isPrepaid = elder.treatStatus === 'waiting_period';
|
||||
const record = await this.prisma.client.record.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
createdBy: ctx.userId,
|
||||
orderId: order.id,
|
||||
elderId: order.elderId,
|
||||
assigneeId: order.assigneeId!,
|
||||
checkinTime: new Date(), // 服务器取时,忽略客户端传入
|
||||
checkinLat: dto.checkinLat ?? null,
|
||||
checkinLng: dto.checkinLng ?? null,
|
||||
locateStatus: dto.locateStatus ?? 'ok', // 定位失败/拒绝不阻塞(3002)
|
||||
recordStatus: 'serving',
|
||||
isPrepaid,
|
||||
billable: !isPrepaid,
|
||||
},
|
||||
// 「工单转 serving + 建 serving 留痕」同一事务原子落库(仅 assigned → serving,serving 不重复转)。
|
||||
const record = await this.prisma.client.$transaction(async (tx) => {
|
||||
if (order.orderStatus === 'assigned') {
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: { orderStatus: 'serving' },
|
||||
});
|
||||
}
|
||||
return tx.record.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
createdBy: ctx.userId,
|
||||
orderId: order.id,
|
||||
elderId: order.elderId,
|
||||
assigneeId: order.assigneeId!,
|
||||
checkinTime: new Date(), // 服务器取时,忽略客户端传入
|
||||
checkinLat: dto.checkinLat ?? null,
|
||||
checkinLng: dto.checkinLng ?? null,
|
||||
locateStatus: dto.locateStatus ?? 'ok', // 定位失败/拒绝不阻塞(3002)
|
||||
recordStatus: 'serving',
|
||||
isPrepaid,
|
||||
billable: !isPrepaid,
|
||||
},
|
||||
});
|
||||
});
|
||||
return this.serializeRecord(record);
|
||||
}
|
||||
@ -168,14 +177,21 @@ export class RecordService {
|
||||
const durationMin = checkinTime
|
||||
? Math.round((checkoutTime.getTime() - new Date(checkinTime).getTime()) / 60000)
|
||||
: null;
|
||||
const updated = await this.prisma.client.record.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
checkoutTime,
|
||||
durationMin,
|
||||
recordStatus: 'submitted',
|
||||
submitTime: new Date(),
|
||||
},
|
||||
// 「工单转 done + 留痕 submitted」同一事务原子落库。
|
||||
const updated = await this.prisma.client.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id: record.orderId },
|
||||
data: { orderStatus: 'done' },
|
||||
});
|
||||
return tx.record.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
checkoutTime,
|
||||
durationMin,
|
||||
recordStatus: 'submitted',
|
||||
submitTime: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
return this.serializeRecord(updated);
|
||||
}
|
||||
@ -212,58 +228,67 @@ export class RecordService {
|
||||
const durationMin =
|
||||
checkinTime && checkoutTime ? Math.round((checkoutTime.getTime() - checkinTime.getTime()) / 60000) : null;
|
||||
|
||||
const record = await this.prisma.client.record.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
createdBy: ctx.userId,
|
||||
orderId: order.id,
|
||||
elderId: order.elderId,
|
||||
assigneeId: order.assigneeId!,
|
||||
checkinTime,
|
||||
checkoutTime,
|
||||
durationMin,
|
||||
checkinLat: dto.checkinLat ?? null,
|
||||
checkinLng: dto.checkinLng ?? null,
|
||||
locateStatus: dto.locateStatus ?? 'ok',
|
||||
recordStatus: 'submitted',
|
||||
offlineFlag: true,
|
||||
clientMd5: dto.clientMd5,
|
||||
submitTime: new Date(),
|
||||
isPrepaid,
|
||||
billable: !isPrepaid,
|
||||
},
|
||||
});
|
||||
|
||||
// 项目解析为只读操作(读 L4/快照),放在事务外;写入与工单状态流转同一事务原子落库。
|
||||
const resolved = await this.resolveItems(dto.items ?? [], order);
|
||||
for (const item of resolved) {
|
||||
await this.prisma.client.recordItem.create({
|
||||
|
||||
const record = await this.prisma.client.$transaction(async (tx) => {
|
||||
// 按 checkin/checkout 语义更新工单:含 checkoutTime → done,仅签到 → serving。
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: { orderStatus: checkoutTime ? 'done' : 'serving' },
|
||||
});
|
||||
const created = await tx.record.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
recordId: record.id,
|
||||
svcId: item.svcId,
|
||||
svcNameSnapshot: item.svcName,
|
||||
execResult: item.execResult,
|
||||
undoReason: item.undoReason ?? null,
|
||||
extraFlag: item.extraFlag ?? false,
|
||||
photoRequired: item.photoRequired,
|
||||
isRefusedPhoto: item.isRefusedPhoto ?? false,
|
||||
refuseReason: item.refuseReason ?? null,
|
||||
createdBy: ctx.userId,
|
||||
orderId: order.id,
|
||||
elderId: order.elderId,
|
||||
assigneeId: order.assigneeId!,
|
||||
checkinTime,
|
||||
checkoutTime,
|
||||
durationMin,
|
||||
checkinLat: dto.checkinLat ?? null,
|
||||
checkinLng: dto.checkinLng ?? null,
|
||||
locateStatus: dto.locateStatus ?? 'ok',
|
||||
recordStatus: 'submitted',
|
||||
offlineFlag: true,
|
||||
clientMd5: dto.clientMd5,
|
||||
submitTime: new Date(),
|
||||
isPrepaid,
|
||||
billable: !isPrepaid,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const photo of dto.photos ?? []) {
|
||||
await this.prisma.client.recordPhoto.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
recordId: record.id,
|
||||
svcId: photo.svcId ?? null,
|
||||
filePath: photo.filePath,
|
||||
fileSize: photo.fileSize,
|
||||
visibleToFamily: false,
|
||||
keepUntil: new Date(Date.now() + PHOTO_KEEP_MS),
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const item of resolved) {
|
||||
await tx.recordItem.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
recordId: created.id,
|
||||
svcId: item.svcId,
|
||||
svcNameSnapshot: item.svcName,
|
||||
execResult: item.execResult,
|
||||
undoReason: item.undoReason ?? null,
|
||||
extraFlag: item.extraFlag ?? false,
|
||||
photoRequired: item.photoRequired,
|
||||
isRefusedPhoto: item.isRefusedPhoto ?? false,
|
||||
refuseReason: item.refuseReason ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const photo of dto.photos ?? []) {
|
||||
await tx.recordPhoto.create({
|
||||
data: {
|
||||
tenantCode: ctx.tenantCode,
|
||||
recordId: created.id,
|
||||
svcId: photo.svcId ?? null,
|
||||
filePath: photo.filePath,
|
||||
fileSize: photo.fileSize,
|
||||
visibleToFamily: false,
|
||||
keepUntil: new Date(Date.now() + PHOTO_KEEP_MS),
|
||||
},
|
||||
});
|
||||
}
|
||||
return created;
|
||||
});
|
||||
return this.serializeRecord(record);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user