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,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
|
IsIn,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
IsObject,
|
||||||
@@ -362,5 +363,6 @@ export class MergePipelineDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsIn(['retarget_all'])
|
||||||
leadFormMode?: '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 movedDeals = 0;
|
||||||
let retargetedLeadForms = 0;
|
let retargetedLeadForms = 0;
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(
|
||||||
const closedAt = new Date();
|
async (tx) => {
|
||||||
for (const sourceStage of source.stages) {
|
const closedAt = new Date();
|
||||||
const targetStageId = dto.stageMap[sourceStage.id];
|
for (const sourceStage of source.stages) {
|
||||||
const targetStage = targetStagesById.get(targetStageId);
|
const targetStageId = dto.stageMap[sourceStage.id];
|
||||||
if (!targetStage) {
|
const targetStage = targetStagesById.get(targetStageId);
|
||||||
throw new BadRequestException('نگاشت ستونها نامعتبر است');
|
if (!targetStage) {
|
||||||
}
|
throw new BadRequestException('نگاشت ستونها نامعتبر است');
|
||||||
const status = dealStatusForStage(targetStage);
|
}
|
||||||
|
const status = dealStatusForStage(targetStage);
|
||||||
|
|
||||||
if (status === 'open') {
|
if (status === 'open') {
|
||||||
const result = await tx.deal.updateMany({
|
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: {
|
where: {
|
||||||
organizationId,
|
organizationId,
|
||||||
pipelineId: source.id,
|
pipelineId: source.id,
|
||||||
stageId: sourceStage.id,
|
stageId: sourceStage.id,
|
||||||
|
status: 'open',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
pipelineId: target.id,
|
pipelineId: target.id,
|
||||||
stageId: targetStage.id,
|
stageId: targetStage.id,
|
||||||
status,
|
status,
|
||||||
closedAt: null,
|
closedAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
movedDeals += result.count;
|
const alreadyClosed = await tx.deal.updateMany({
|
||||||
continue;
|
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({
|
const leadForms = await tx.leadForm.updateMany({
|
||||||
where: {
|
where: { organizationId, pipelineId: source.id },
|
||||||
organizationId,
|
data: { pipelineId: target.id },
|
||||||
pipelineId: source.id,
|
|
||||||
stageId: sourceStage.id,
|
|
||||||
status: 'open',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
pipelineId: target.id,
|
|
||||||
stageId: targetStage.id,
|
|
||||||
status,
|
|
||||||
closedAt,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const alreadyClosed = await tx.deal.updateMany({
|
retargetedLeadForms = leadForms.count;
|
||||||
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 leadForms = await tx.leadForm.updateMany({
|
if (source.isDefault) {
|
||||||
where: { organizationId, pipelineId: source.id },
|
await tx.pipeline.updateMany({
|
||||||
data: { pipelineId: target.id },
|
where: { organizationId, isDefault: true },
|
||||||
});
|
data: { isDefault: false },
|
||||||
retargetedLeadForms = leadForms.count;
|
});
|
||||||
|
await tx.pipeline.update({
|
||||||
|
where: { id: target.id },
|
||||||
|
data: { isDefault: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (source.isDefault) {
|
const remaining = await tx.deal.count({
|
||||||
await tx.pipeline.updateMany({
|
where: { organizationId, pipelineId: source.id },
|
||||||
where: { organizationId, isDefault: true },
|
|
||||||
data: { isDefault: false },
|
|
||||||
});
|
});
|
||||||
await tx.pipeline.update({
|
if (remaining > 0) {
|
||||||
where: { id: target.id },
|
throw new BadRequestException(
|
||||||
data: { isDefault: true },
|
'برخی معاملهها منتقل نشدند؛ ادغام لغو شد.',
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.pipeline.delete({ where: { id: source.id } });
|
await tx.pipeline.delete({ where: { id: source.id } });
|
||||||
});
|
},
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user