fix(api): guard unmoved deals before pipeline merge delete
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Task 5 report
|
||||
Status: DONE
|
||||
- shared build: OK
|
||||
- pipeline-merge.test.ts: 5/5 pass
|
||||
- api typecheck: OK
|
||||
- vue-tsc: see VUE_EXIT in controller log
|
||||
- graphify: attempted (may crash)
|
||||
No commit required for verification-only task.
|
||||
|
||||
## Follow-up fix: final review important findings
|
||||
Status: DONE
|
||||
- I-1: Added in-transaction unmoved-deal count guard before deleting the source pipeline.
|
||||
- I-3: Added 30000ms timeout to the interactive merge transaction.
|
||||
- M-1: Added `@IsIn(['retarget_all'])` validation to `MergePipelineDto.leadFormMode`.
|
||||
- graphify update: OK
|
||||
- `cd apps/api; npx tsx --test test/pipeline-merge.test.ts`: OK, 5/5 pass
|
||||
- `npm run typecheck -w @mento/api`: OK
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
@@ -362,5 +363,6 @@ export class MergePipelineDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['retarget_all'])
|
||||
leadFormMode?: 'retarget_all';
|
||||
}
|
||||
|
||||
@@ -416,87 +416,101 @@ export class PipelinesService {
|
||||
);
|
||||
}
|
||||
|
||||
const targetStagesById = new Map(target.stages.map((stage) => [stage.id, stage]));
|
||||
const targetStagesById = new Map(
|
||||
target.stages.map((stage) => [stage.id, stage]),
|
||||
);
|
||||
let movedDeals = 0;
|
||||
let retargetedLeadForms = 0;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const closedAt = new Date();
|
||||
for (const sourceStage of source.stages) {
|
||||
const targetStageId = dto.stageMap[sourceStage.id];
|
||||
const targetStage = targetStagesById.get(targetStageId);
|
||||
if (!targetStage) {
|
||||
throw new BadRequestException('نگاشت ستونها نامعتبر است');
|
||||
}
|
||||
const status = dealStatusForStage(targetStage);
|
||||
await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const closedAt = new Date();
|
||||
for (const sourceStage of source.stages) {
|
||||
const targetStageId = dto.stageMap[sourceStage.id];
|
||||
const targetStage = targetStagesById.get(targetStageId);
|
||||
if (!targetStage) {
|
||||
throw new BadRequestException('نگاشت ستونها نامعتبر است');
|
||||
}
|
||||
const status = dealStatusForStage(targetStage);
|
||||
|
||||
if (status === 'open') {
|
||||
const result = await tx.deal.updateMany({
|
||||
if (status === 'open') {
|
||||
const result = await tx.deal.updateMany({
|
||||
where: {
|
||||
organizationId,
|
||||
pipelineId: source.id,
|
||||
stageId: sourceStage.id,
|
||||
},
|
||||
data: {
|
||||
pipelineId: target.id,
|
||||
stageId: targetStage.id,
|
||||
status,
|
||||
closedAt: null,
|
||||
},
|
||||
});
|
||||
movedDeals += result.count;
|
||||
continue;
|
||||
}
|
||||
|
||||
const opened = await tx.deal.updateMany({
|
||||
where: {
|
||||
organizationId,
|
||||
pipelineId: source.id,
|
||||
stageId: sourceStage.id,
|
||||
status: 'open',
|
||||
},
|
||||
data: {
|
||||
pipelineId: target.id,
|
||||
stageId: targetStage.id,
|
||||
status,
|
||||
closedAt: null,
|
||||
closedAt,
|
||||
},
|
||||
});
|
||||
movedDeals += result.count;
|
||||
continue;
|
||||
const alreadyClosed = await tx.deal.updateMany({
|
||||
where: {
|
||||
organizationId,
|
||||
pipelineId: source.id,
|
||||
stageId: sourceStage.id,
|
||||
status: { not: 'open' },
|
||||
},
|
||||
data: {
|
||||
pipelineId: target.id,
|
||||
stageId: targetStage.id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
movedDeals += opened.count + alreadyClosed.count;
|
||||
}
|
||||
|
||||
const opened = await tx.deal.updateMany({
|
||||
where: {
|
||||
organizationId,
|
||||
pipelineId: source.id,
|
||||
stageId: sourceStage.id,
|
||||
status: 'open',
|
||||
},
|
||||
data: {
|
||||
pipelineId: target.id,
|
||||
stageId: targetStage.id,
|
||||
status,
|
||||
closedAt,
|
||||
},
|
||||
const leadForms = await tx.leadForm.updateMany({
|
||||
where: { organizationId, pipelineId: source.id },
|
||||
data: { pipelineId: target.id },
|
||||
});
|
||||
const alreadyClosed = await tx.deal.updateMany({
|
||||
where: {
|
||||
organizationId,
|
||||
pipelineId: source.id,
|
||||
stageId: sourceStage.id,
|
||||
status: { not: 'open' },
|
||||
},
|
||||
data: {
|
||||
pipelineId: target.id,
|
||||
stageId: targetStage.id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
movedDeals += opened.count + alreadyClosed.count;
|
||||
}
|
||||
retargetedLeadForms = leadForms.count;
|
||||
|
||||
const leadForms = await tx.leadForm.updateMany({
|
||||
where: { organizationId, pipelineId: source.id },
|
||||
data: { pipelineId: target.id },
|
||||
});
|
||||
retargetedLeadForms = leadForms.count;
|
||||
if (source.isDefault) {
|
||||
await tx.pipeline.updateMany({
|
||||
where: { organizationId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
await tx.pipeline.update({
|
||||
where: { id: target.id },
|
||||
data: { isDefault: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (source.isDefault) {
|
||||
await tx.pipeline.updateMany({
|
||||
where: { organizationId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
const remaining = await tx.deal.count({
|
||||
where: { organizationId, pipelineId: source.id },
|
||||
});
|
||||
await tx.pipeline.update({
|
||||
where: { id: target.id },
|
||||
data: { isDefault: true },
|
||||
});
|
||||
}
|
||||
if (remaining > 0) {
|
||||
throw new BadRequestException(
|
||||
'برخی معاملهها منتقل نشدند؛ ادغام لغو شد.',
|
||||
);
|
||||
}
|
||||
|
||||
await tx.pipeline.delete({ where: { id: source.id } });
|
||||
});
|
||||
await tx.pipeline.delete({ where: { id: source.id } });
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user