feat: change minmax probability to minmax score

This commit is contained in:
2026-09-20 16:05:16 +03:30
parent 7658ba805d
commit f660a6b7a7
13 changed files with 108 additions and 58 deletions
+1 -1
View File
@@ -4,6 +4,6 @@
}, },
"errors": { "errors": {
"resultRangeOverlappingConstraint": "The specified score range has some overlaps with other records.", "resultRangeOverlappingConstraint": "The specified score range has some overlaps with other records.",
"resultRangeMinMaxConstraint": "minProbability value should be less than maxProbability." "resultRangeMinMaxConstraint": "minScore value should be less than maxScore."
} }
} }
+1 -1
View File
@@ -4,6 +4,6 @@
}, },
"errors": { "errors": {
"resultRangeOverlappingConstraint": "بازه امتیاز مشخص شده با بازه های دیگر تداخل دارد.", "resultRangeOverlappingConstraint": "بازه امتیاز مشخص شده با بازه های دیگر تداخل دارد.",
"resultRangeMinMaxConstraint": "minProbability باید کمتر از maxProbability باشد." "resultRangeMinMaxConstraint": "minScore باید کمتر از maxScore باشد."
} }
} }
@@ -1,20 +1,35 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from 'typeorm';
export class ChangeResultRangeFields1788950646073 implements MigrationInterface { export class ChangeResultRangeFields1788950646073 implements MigrationInterface {
name = 'ChangeResultRangeFields1788950646073' name = 'ChangeResultRangeFields1788950646073';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "result_range" DROP COLUMN "minScore"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "result_range" DROP COLUMN "maxScore"`); `ALTER TABLE "result_range" DROP COLUMN "minScore"`,
await queryRunner.query(`ALTER TABLE "result_range" ADD "minProbability" integer NOT NULL`); );
await queryRunner.query(`ALTER TABLE "result_range" ADD "maxProbability" integer NOT NULL`); await queryRunner.query(
} `ALTER TABLE "result_range" DROP COLUMN "maxScore"`,
);
public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(
await queryRunner.query(`ALTER TABLE "result_range" DROP COLUMN "maxProbability"`); `ALTER TABLE "result_range" ADD "minProbability" integer NOT NULL`,
await queryRunner.query(`ALTER TABLE "result_range" DROP COLUMN "minProbability"`); );
await queryRunner.query(`ALTER TABLE "result_range" ADD "maxScore" integer NOT NULL`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "result_range" ADD "minScore" integer NOT NULL`); `ALTER TABLE "result_range" ADD "maxProbability" integer NOT NULL`,
} );
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "maxProbability"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "minProbability"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "maxScore" integer NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "minScore" integer NOT NULL`,
);
}
} }
@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CalculateByScore1789906979798 implements MigrationInterface {
name = 'CalculateByScore1789906979798';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "minProbability"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "maxProbability"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "minScore" integer NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "maxScore" integer NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "maxScore"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" DROP COLUMN "minScore"`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "maxProbability" integer NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "result_range" ADD "minProbability" integer NOT NULL`,
);
}
}
@@ -6,6 +6,6 @@ Content-Type: application/json
"test": 4, "test": 4,
"title": "احتمال خیلی بالا", "title": "احتمال خیلی بالا",
"description": "احتمال ابتلا خیلی بالا است.", "description": "احتمال ابتلا خیلی بالا است.",
"minProbability": 21, "minScore": 21,
"maxProbability": 40 "maxScore": 40
} }
@@ -8,11 +8,11 @@ export class CreateResultRangeDTO {
@IsNumber() @IsNumber()
@IsNotEmpty() @IsNotEmpty()
minProbability!: number; minScore!: number;
@IsNumber() @IsNumber()
@IsNotEmpty() @IsNotEmpty()
maxProbability!: number; maxScore!: number;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -14,11 +14,11 @@ export class FindResultRangeDTO extends PartialType(PaginationQueryDTO) {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
minProbability?: number; minScore?: number;
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
maxProbability?: number; maxScore?: number;
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -15,13 +15,13 @@ export class ResultRange {
type: 'int', type: 'int',
nullable: false, nullable: false,
}) })
minProbability!: number; minScore!: number;
@Column({ @Column({
type: 'int', type: 'int',
nullable: false, nullable: false,
}) })
maxProbability!: number; maxScore!: number;
@Column({ @Column({
type: 'varchar', type: 'varchar',
@@ -1,8 +1,8 @@
export enum ResultRangeSortField { export enum ResultRangeSortField {
ID = 'id', ID = 'id',
TEST = 'test', TEST = 'test',
MIN_PROBABILITY = 'minProbability', MIN_SCORE = 'minScore',
MAX_PROBABILITY = 'maxProbability', MAX_SCORE = 'maxScore',
TITLE = 'title', TITLE = 'title',
DESCRIPTION = 'description', DESCRIPTION = 'description',
} }
@@ -17,12 +17,12 @@ export const filterResultRanges = (query: FindResultRangeDTO) => {
where.title = ILike(`%${query.title}%`); where.title = ILike(`%${query.title}%`);
} }
if (query.maxProbability !== undefined) { if (query.maxScore !== undefined) {
where.maxProbability = query.maxProbability; where.maxScore = query.maxScore;
} }
if (query.minProbability !== undefined) { if (query.minScore !== undefined) {
where.minProbability = query.minProbability; where.minScore = query.minScore;
} }
return where; return where;
@@ -32,16 +32,16 @@ export class ResultRangeCommandProvider {
private async checkRangeOverlap( private async checkRangeOverlap(
test: number, test: number,
minProbability: number, minScore: number,
maxProbability: number, maxScore: number,
excludeSelf: boolean = false, excludeSelf: boolean = false,
): Promise<void> { ): Promise<void> {
const queryBuilder = this.resultRangeRepository const queryBuilder = this.resultRangeRepository
.createQueryBuilder('resultRange') .createQueryBuilder('resultRange')
.where('resultRange.testId = :test', { test }) .where('resultRange.testId = :test', { test })
.andWhere( .andWhere(
'(resultRange.minProbability BETWEEN :minProbability AND :maxProbability) OR (resultRange.maxProbability BETWEEN :minProbability AND :maxProbability) OR (:minProbability BETWEEN resultRange.minProbability AND resultRange.maxProbability) OR (:maxProbability BETWEEN resultRange.minProbability AND resultRange.maxProbability)', '(resultRange.minScore BETWEEN :minScore AND :maxScore) OR (resultRange.maxScore BETWEEN :minScore AND :maxScore) OR (:minScore BETWEEN resultRange.minScore AND resultRange.maxScore) OR (:maxScore BETWEEN resultRange.minScore AND resultRange.maxScore)',
{ minProbability, maxProbability }, { minScore, maxScore },
); );
let overlappingRanges: Array<ResultRange>; let overlappingRanges: Array<ResultRange>;
@@ -92,8 +92,8 @@ export class ResultRangeCommandProvider {
await this.checkRangeOverlap( await this.checkRangeOverlap(
test.id, test.id,
createResultRangeDto.minProbability, createResultRangeDto.minScore,
createResultRangeDto.maxProbability, createResultRangeDto.maxScore,
); );
const newResultRange = this.resultRangeRepository.create({ const newResultRange = this.resultRangeRepository.create({
@@ -129,23 +129,23 @@ export class ResultRangeCommandProvider {
? updateResultRangeDto.test ? updateResultRangeDto.test
: resultRange.test.id; : resultRange.test.id;
const minProbability = const minScore =
updateResultRangeDto.minProbability !== undefined updateResultRangeDto.minScore !== undefined
? updateResultRangeDto.minProbability ? updateResultRangeDto.minScore
: resultRange.minProbability; : resultRange.minScore;
const maxProbability = const maxScore =
updateResultRangeDto.maxProbability !== undefined updateResultRangeDto.maxScore !== undefined
? updateResultRangeDto.maxProbability ? updateResultRangeDto.maxScore
: resultRange.maxProbability; : resultRange.maxScore;
if (minProbability >= maxProbability) { if (minScore >= maxScore) {
throw new BadRequestException( throw new BadRequestException(
'scoring.errors.resultRangeMinMaxConstraint', 'scoring.errors.resultRangeMinMaxConstraint',
); );
} }
await this.checkRangeOverlap(testId, minProbability, maxProbability, true); await this.checkRangeOverlap(testId, minScore, maxScore, true);
Object.assign(resultRange, updateResultRangeDto); Object.assign(resultRange, updateResultRangeDto);
@@ -14,15 +14,15 @@ export class ResultRangeValidator implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const obj = validationArguments?.object as any; const obj = validationArguments?.object as any;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { minProbability, maxProbability } = obj; const { minScore, maxScore } = obj;
return minProbability < maxProbability; return minScore < maxScore;
} }
defaultMessage?(): string { defaultMessage?(): string {
const i18n = I18nContext.current(); const i18n = I18nContext.current();
return ( return (
i18n?.t('scoring.errors.resultRangeMinMaxConstraint') || i18n?.t('scoring.errors.resultRangeMinMaxConstraint') ||
'minProbability value should be less than maxProbability.' 'minScore value should be less than maxScore.'
); );
} }
} }
@@ -72,12 +72,12 @@ export class CalculateResultProvider {
rangeRules: true, rangeRules: true,
}, },
}), }),
async (probability) => async (score) =>
manager.find(ResultRange, { manager.find(ResultRange, {
where: { where: {
test: { id: submission.test.id }, test: { id: submission.test.id },
minProbability: LessThanOrEqual(probability), minScore: LessThanOrEqual(score),
maxProbability: MoreThanOrEqual(probability), maxScore: MoreThanOrEqual(score),
}, },
}), }),
); );
@@ -119,12 +119,12 @@ export class CalculateResultProvider {
rangeRules: true, rangeRules: true,
}, },
}), }),
async (probability) => async (score) =>
this.resultRangeRepository.find({ this.resultRangeRepository.find({
where: { where: {
test: { id: submission.testId }, test: { id: submission.testId },
minProbability: LessThanOrEqual(probability), minScore: LessThanOrEqual(score),
maxProbability: MoreThanOrEqual(probability), maxScore: MoreThanOrEqual(score),
}, },
}), }),
); );
@@ -150,7 +150,7 @@ export class CalculateResultProvider {
numericValue?: number; numericValue?: number;
}>, }>,
loadQuestions: (questionIds: number[]) => Promise<Question[]>, loadQuestions: (questionIds: number[]) => Promise<Question[]>,
loadResultRanges: (probability: number) => Promise<ResultRange[]>, loadResultRanges: (score: number) => Promise<ResultRange[]>,
) { ) {
let score = 0; let score = 0;
@@ -242,7 +242,7 @@ export class CalculateResultProvider {
const probability = const probability =
totalScores > 0 ? Math.ceil((score / totalScores) * 100) : 0; totalScores > 0 ? Math.ceil((score / totalScores) * 100) : 0;
const resultRanges = await loadResultRanges(probability); const resultRanges = await loadResultRanges(score);
return { return {
score, score,