fix(api): guard unmoved deals before pipeline merge delete

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 21:32:21 +03:30
co-authored by Cursor
parent fd6e79c15d
commit 9bed6cac03
3 changed files with 92 additions and 59 deletions
+2
View File
@@ -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';
}
+73 -59
View File
@@ -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,