diff --git a/prisma/migrations/20230920070531_add_float/migration.sql b/prisma/migrations/20230920070531_add_float/migration.sql new file mode 100644 index 00000000..0470981e --- /dev/null +++ b/prisma/migrations/20230920070531_add_float/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "IndividualResult" ALTER COLUMN "diffPoints" SET DATA TYPE DOUBLE PRECISION, +ALTER COLUMN "pointsToAdd" SET DATA TYPE DOUBLE PRECISION; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f6e42058..46fc187f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -62,8 +62,8 @@ model IndividualResult { competitionCoef Int competitionType CompetitionType competitionName String - diffPoints Int - pointsToAdd Int + diffPoints Float + pointsToAdd Float looseFactor Float definitivePointsToAdd Int member Member @relation("member", fields: [memberId, memberLicence], references: [id, licence]) diff --git a/src/api/member/controllers/member.controller.ts b/src/api/member/controllers/member.controller.ts index a3bee5db..f17bc8ad 100644 --- a/src/api/member/controllers/member.controller.ts +++ b/src/api/member/controllers/member.controller.ts @@ -21,7 +21,8 @@ import { WeeklyNumericRanking, WeeklyNumericRankingInput, WeeklyNumericRankingInputV2, - WeeklyNumericRankingV2, WeeklyNumericRankingV3, + WeeklyNumericRankingV2, + WeeklyNumericRankingV3, } from '../dto/member.dto'; import { PlayerCategory } from '../../../entity/tabt-input.interface'; import { EloMemberService } from '../../../services/members/elo-member.service'; @@ -29,6 +30,7 @@ import { SeasonService } from '../../../services/seasons/season.service'; import { MembersSearchIndexService } from '../../../services/members/members-search-index.service'; import { MemberCategoryService } from '../../../services/members/member-category.service'; import { getSimplifiedPlayerCategory } from '../helpers/player-category-helpers'; +import { NumericRankingService } from 'src/common/data-aftt/services/numeric-ranking.service'; @ApiTags('Members') @Controller({ @@ -43,6 +45,7 @@ export class MemberController { private readonly eloMemberService: EloMemberService, private readonly seasonService: SeasonService, private readonly membersSearchIndexService: MembersSearchIndexService, + private readonly numericRankingService: NumericRankingService ) { } @@ -218,6 +221,25 @@ export class MemberController { return numericRankingV3; } + @Get(':uniqueIndex/numeric-rankings') + @ApiOkResponse({ + type: WeeklyNumericRankingV3, + description: 'The list of ELO points for a player in a season', + }) + @ApiOperation({ + operationId: 'findMemberNumericRankingsHistoryV3', + }) + @ApiNotFoundResponse({ + description: 'No points found for given player', + }) + @Version('4') + async findNumericRankingV4( + @Param('uniqueIndex', ParseIntPipe) id: number, + @Query() params: WeeklyNumericRankingInputV2, + ) { + const simplifiedCategory = getSimplifiedPlayerCategory(params.category); + return await this.numericRankingService.getWeeklyRanking(id, simplifiedCategory); + } } diff --git a/src/api/member/dto/member.dto.ts b/src/api/member/dto/member.dto.ts index bcf85ca4..5d16cdf1 100644 --- a/src/api/member/dto/member.dto.ts +++ b/src/api/member/dto/member.dto.ts @@ -163,6 +163,8 @@ export class WeeklyNumericRankingV3 { perDateHistory: NumericRankingDetailsV3[]; } +export type WeeklyNumericRankingV4 = WeeklyNumericRankingV3; + export enum PLAYER_CATEGORY { MEN = 'MEN', WOMEN = 'WOMEN', diff --git a/src/common/common.module.ts b/src/common/common.module.ts index cf6840eb..025a0538 100644 --- a/src/common/common.module.ts +++ b/src/common/common.module.ts @@ -25,6 +25,7 @@ import { PrismaService } from './prisma.service'; import { DataAFTTMemberNumericRankingModel } from './data-aftt/model/member-numeric-ranking.model'; import { DataAFTTMemberProcessingService } from './data-aftt/services/member-processing.service'; import { DataAFTTResultsProcessingService } from './data-aftt/services/results-processing.service'; +import { NumericRankingService } from './data-aftt/services/numeric-ranking.service'; const asyncProviders: Provider[] = [ @@ -90,7 +91,8 @@ const asyncProviders: Provider[] = [ DataAFTTMemberNumericRankingModel, PrismaService, DataAFTTMemberProcessingService, - DataAFTTResultsProcessingService + DataAFTTResultsProcessingService, + NumericRankingService ], exports: [ ...asyncProviders, @@ -105,7 +107,8 @@ const asyncProviders: Provider[] = [ DataAFTTMemberModel, DataAFTTMemberNumericRankingModel, DataAFTTMemberProcessingService, - DataAFTTResultsProcessingService + DataAFTTResultsProcessingService, + NumericRankingService ], }) export class CommonModule { diff --git a/src/common/data-aftt/model/individual-results.model.ts b/src/common/data-aftt/model/individual-results.model.ts index 905b5999..a8ff9e20 100644 --- a/src/common/data-aftt/model/individual-results.model.ts +++ b/src/common/data-aftt/model/individual-results.model.ts @@ -1,6 +1,12 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "../../prisma.service"; -import { Gender, IndividualResult, Member } from "@prisma/client"; +import { Gender, IndividualResult, Member, Prisma } from "@prisma/client"; + +export type IndividualResultWithOpponent = Prisma.IndividualResultGetPayload<{ + include: { + memberOpponent: true + } +}>; @Injectable() export class DataAFTTIndividualResultModel { @@ -9,6 +15,23 @@ export class DataAFTTIndividualResultModel { ) { } + async getResults( + licence: number, + gender: Gender + ): Promise { + return this.prismaService.individualResult.findMany({ + where: { + memberLicence: licence, + member: { + gender + } + }, + include: { + memberOpponent: true + } + }); + } + async upsert( result: Omit, 'opponentId'>, gender: Gender @@ -29,11 +52,26 @@ export class DataAFTTIndividualResultModel { }) ]); + return this.prismaService.individualResult.upsert({ where: { id: result.id }, update: { + date: result.date, + competitionType: result.competitionType, + score: result.score, + memberRanking: result.memberRanking, + memberPoints: result.memberPoints, + opponentRanking: result.opponentRanking, + opponentPoints: result.opponentPoints, + competitionCoef: result.competitionCoef, + competitionName: result.competitionName, + result: result.result, + definitivePointsToAdd: result.definitivePointsToAdd, + diffPoints: result.diffPoints, + looseFactor: result.looseFactor, + pointsToAdd: result.pointsToAdd, member: { connect: { id_licence: { @@ -88,4 +126,4 @@ export class DataAFTTIndividualResultModel { } -} \ No newline at end of file +} diff --git a/src/common/data-aftt/model/member-numeric-ranking.model.ts b/src/common/data-aftt/model/member-numeric-ranking.model.ts index 17a18139..4e0cc738 100644 --- a/src/common/data-aftt/model/member-numeric-ranking.model.ts +++ b/src/common/data-aftt/model/member-numeric-ranking.model.ts @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "../../prisma.service"; -import { NumericPoints } from "@prisma/client"; +import { Gender, NumericPoints } from "@prisma/client"; @Injectable() export class DataAFTTMemberNumericRankingModel { @@ -10,6 +10,22 @@ export class DataAFTTMemberNumericRankingModel { } + getLatestPoints(licence: number, gender: Gender): Promise { + const points = this.prismaService.numericPoints.findMany({ + where: { + memberLicence: licence, + member: { + gender + } + }, + orderBy: { + date: 'desc' + } + }); + return points; + } + + async insertInHistory(points: NumericPoints): Promise { // get latest point for member // if latest point is different from current point, insert new point. I want to keen track of the evolution of the points diff --git a/src/common/data-aftt/services/member-processing.service.ts b/src/common/data-aftt/services/member-processing.service.ts index 8f92a9c2..5d79e50d 100644 --- a/src/common/data-aftt/services/member-processing.service.ts +++ b/src/common/data-aftt/services/member-processing.service.ts @@ -1,100 +1,96 @@ -import { HttpService } from "@nestjs/axios"; -import { ConfigService } from "@nestjs/config"; -import { DataAFTTMemberModel } from "../model/member.model"; -import { DataAFTTMemberNumericRankingModel } from "../model/member-numeric-ranking.model"; -import { genderMapping } from "../constants"; -import { firstValueFrom } from "rxjs"; -import { Injectable, Logger } from "@nestjs/common"; -import { Gender } from "@prisma/client"; -import * as pqueue from 'p-queue'; -import * as os from 'os'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { DataAFTTMemberModel } from '../model/member.model'; +import { DataAFTTMemberNumericRankingModel } from '../model/member-numeric-ranking.model'; +import { genderMapping } from '../constants'; +import { firstValueFrom } from 'rxjs'; +import { Injectable, Logger } from '@nestjs/common'; +import { Gender } from '@prisma/client'; + @Injectable() export class DataAFTTMemberProcessingService { - private readonly logger = new Logger(DataAFTTMemberProcessingService.name); + private readonly logger = new Logger(DataAFTTMemberProcessingService.name); - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - private readonly memberServiceModel: DataAFTTMemberModel, - private readonly numericRankingModel: DataAFTTMemberNumericRankingModel - ) { - } + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + private readonly memberServiceModel: DataAFTTMemberModel, + private readonly numericRankingModel: DataAFTTMemberNumericRankingModel, + ) { + } - async process(): Promise { - const queue = new pqueue.default({concurrency: (os.cpus().length * 2) + 1}); - for (const [gender, mapping] of genderMapping) { - const file = await this.downloadFile(gender, mapping); - - // split lines and remove last line - const lines = file.data.split('\n').slice(0, -1); - //console.log(cols); - this.logger.log(`File downloaded, start processing with concurrency ${queue.concurrency}...`); - queue.addAll(lines.map(line => async () => { - const cols = line.split(';'); - return this.updateDB(cols, gender); - })); - console.log(`Processing ${queue.size} lines...`) - await queue.onIdle(); - this.logger.log(`Processing done. (${lines.length} lines)`); - } - } + async process(): Promise { + for (const [gender, mapping] of genderMapping) { + const file = await this.downloadFile(gender, mapping); - private async updateDB(cols: string[], gender: Gender) { - try { - await this.memberServiceModel.upsert({ - id: parseInt(cols[0], 10), - licence: parseInt(cols[1], 10), - gender, - lastname: cols[2], - firstname: cols[3], - ranking: cols[4], - club: cols[5], - category: cols[7], - worldRanking: parseInt(cols[8], 10), - nationality: cols[9], - }); - /* - 17; - 519190; - GERTENBACH; - ANGELIQUE; - B2; - 5 - A062; - 6 - 0; - 7 - SEN; - 8 - 999; - 9 - NL; - 10 - 2205 - Ranking_Pos 11 - ; - Ranking_Pos_WI - 12 - ; - RankingAn - 13 - 153; - */ - await this.numericRankingModel.insertInHistory({ - memberId: parseInt(cols[0], 10), - memberLicence: parseInt(cols[1], 10), - date: new Date(), - points: parseInt(cols[10], 10), - ranking: cols[11].length ? parseInt(cols[11]) : null, - rankingWI: cols[12].length ? parseInt(cols[12]) : null, - rankingLetterEstimation: null - }); - } catch (e) { - this.logger.error(e.message); - } + // split lines and remove last line + const lines = file.data.split('\n').slice(0, -1); + //console.log(cols); + this.logger.log(`File downloaded, start processing ${lines.length} lines...`); + for (const line of lines) { + const cols = line.split(';'); + return this.updateDB(cols, gender); + } + this.logger.log(`Processing done. (${lines.length} lines)`); } + } - private async downloadFile(gender: string, mapping: string) { - this.logger.log(`Downloading ${gender} file from data.aftt.be`); - const url = `https://data.aftt.be/export/liste_joueurs_${mapping}.txt`; - const file = await firstValueFrom(this.httpService.get(url, { - auth: { - username: this.configService.get('AFTT_DATA_USERNAME'), - password: this.configService.get('AFTT_DATA_PASSWORD') - }, - responseType: 'text' - })); - return file; + private async updateDB(cols: string[], gender: Gender) { + try { + await this.memberServiceModel.upsert({ + id: parseInt(cols[0], 10), + licence: parseInt(cols[1], 10), + gender, + lastname: cols[2], + firstname: cols[3], + ranking: cols[4], + club: cols[5], + category: cols[7], + worldRanking: parseInt(cols[8], 10), + nationality: cols[9], + }); + /* + 17; + 519190; + GERTENBACH; + ANGELIQUE; + B2; + 5 - A062; + 6 - 0; + 7 - SEN; + 8 - 999; + 9 - NL; + 10 - 2205 + Ranking_Pos 11 - ; + Ranking_Pos_WI - 12 - ; + RankingAn - 13 - 153; + */ + await this.numericRankingModel.insertInHistory({ + memberId: parseInt(cols[0], 10), + memberLicence: parseInt(cols[1], 10), + date: new Date(), + points: parseInt(cols[10], 10), + ranking: cols[11].length ? parseInt(cols[11]) : null, + rankingWI: cols[12].length ? parseInt(cols[12]) : null, + rankingLetterEstimation: null, + }); + } catch (e) { + this.logger.error(e.message); } -} \ No newline at end of file + } + + private async downloadFile(gender: string, mapping: string) { + this.logger.log(`Downloading ${gender} file from data.aftt.be`); + const url = `https://data.aftt.be/export/liste_joueurs_${mapping}.txt`; + const file = await firstValueFrom(this.httpService.get(url, { + auth: { + username: this.configService.get('AFTT_DATA_USERNAME'), + password: this.configService.get('AFTT_DATA_PASSWORD'), + }, + responseType: 'text', + })); + return file; + } +} diff --git a/src/common/data-aftt/services/numeric-ranking.service.ts b/src/common/data-aftt/services/numeric-ranking.service.ts new file mode 100644 index 00000000..7cd51288 --- /dev/null +++ b/src/common/data-aftt/services/numeric-ranking.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from "@nestjs/common"; +import { DataAFTTMemberNumericRankingModel } from "../model/member-numeric-ranking.model"; +import { COMPETITION_TYPE, NumericRankingDetailsV3, PLAYER_CATEGORY, WeeklyNumericPointsV3, WeeklyNumericRankingV4 } from "src/api/member/dto/member.dto"; +import { SimplifiedPlayerCategory } from "src/api/member/helpers/player-category-helpers"; +import { PlayerCategory } from "src/entity/tabt-input.interface"; +import { CompetitionType, Gender, IndividualResult } from "@prisma/client"; +import { format } from "date-fns"; +import { DataAFTTIndividualResultModel, IndividualResultWithOpponent } from "../model/individual-results.model"; +import { IndividualMatchResult } from "src/entity/tabt-soap/TabTAPI_Port"; + +@Injectable() +export class NumericRankingService { + constructor( + private readonly memberNumericRankingModel: DataAFTTMemberNumericRankingModel, + private readonly resultHistoryModel: DataAFTTIndividualResultModel + ) { + } + + async getWeeklyRanking(licence: number, simplifiedCategory: SimplifiedPlayerCategory): Promise { + const [points, history] = await Promise.all([ + this.getRankingHistory(licence, simplifiedCategory), + this.getResultsDetailsHistory(licence, simplifiedCategory) + ]); + + return { + perDateHistory: history, + points: points, + } + } + + + async getRankingHistory(licence: number, simplifiedCategory: SimplifiedPlayerCategory): Promise { + const gender = simplifiedCategory === PlayerCategory.MEN ? Gender.MEN : Gender.WOMEN; + const points = await this.memberNumericRankingModel.getLatestPoints(licence, gender); + return points.map(p => ({ + weekName: format(p.date, 'yyyy-MM-dd'), + points: p.points, + })); + } + + async getResultsDetailsHistory(licence: number, simplifiedCategory: SimplifiedPlayerCategory): Promise { + const gender = simplifiedCategory === PlayerCategory.MEN ? Gender.MEN : Gender.WOMEN; + const results = await this.resultHistoryModel.getResults(licence, gender); + + // group result per date and comptetition name + // then for each group, map the points + + const eventGrouped: {[key: string]: IndividualResultWithOpponent[]} = results.reduce<{[key: string]: IndividualResultWithOpponent[]}>((acc, result: IndividualResultWithOpponent) => { + const key = `${result.date}-${result.competitionName}`; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(result); + return acc; + }, {}); + + const eventGroupedArray: NumericRankingDetailsV3[] = Object.keys(eventGrouped).map(key => { + const date = eventGrouped[key][0].date; + const competitionContext = eventGrouped[key][0].competitionName; + const competitionType = eventGrouped[key][0].competitionType === CompetitionType.TOURNAMENT ? COMPETITION_TYPE.TOURNAMENT : COMPETITION_TYPE.CHAMPIONSHIP; + const opponents = eventGrouped[key].map(result => ({ + opponentName: result.memberOpponent.firstname + ' ' + result.memberOpponent.lastname, + opponentRanking: result.opponentRanking, + opponentNumericRanking: result.opponentPoints, + pointsWon: result.definitivePointsToAdd, + score: result.score, + })); + const basePoints = eventGrouped[key][0].memberPoints; + const endPoints = eventGrouped[key].reduce((acc, result) => acc + result.definitivePointsToAdd, eventGrouped[key][0].memberPoints); + return { + date: format(date, 'yyyy-MM-dd'), + competitionContext, + competitionType, + basePoints, + endPoints, + opponents, + } + }); + + return eventGroupedArray; + + } + + +} diff --git a/src/common/data-aftt/services/results-processing.service.ts b/src/common/data-aftt/services/results-processing.service.ts index 7d3815cb..9f9a3d1b 100644 --- a/src/common/data-aftt/services/results-processing.service.ts +++ b/src/common/data-aftt/services/results-processing.service.ts @@ -5,8 +5,6 @@ import { firstValueFrom } from "rxjs"; import { Injectable, Logger, Query } from "@nestjs/common"; import { DataAFTTIndividualResultModel } from "../model/individual-results.model"; import { CompetitionType, Gender, Result } from "@prisma/client"; -import * as pqueue from 'p-queue'; -import * as os from 'os'; @Injectable() export class DataAFTTResultsProcessingService { @@ -22,19 +20,18 @@ export class DataAFTTResultsProcessingService { async process(): Promise { - const queue = new pqueue.default({concurrency: (os.cpus().length * 2) + 1}); for (const [gender, mapping] of genderMapping) { const file = await this.downloadMemberFile(gender, mapping); // split lines and remove last line const lines = file.data.split('\n').slice(0, -1); - this.logger.log(`File downloaded, start processing with concurrency ${queue.concurrency}...`); - queue.addAll(lines.map(line => async () => { + this.logger.log(`File downloaded, start processing ${lines.length} lines...`); + for(const line of lines){ const cols = line.split(';'); await this.updateDB(cols, gender); - })); - + } + /* 1; ID 2023-09-03; DATA @@ -60,11 +57,7 @@ export class DataAFTTResultsProcessingService { 18 - -1 definitive points to add */ - - - this.logger.log(`Processing done. (${lines.length} lines)`); - } } @@ -107,4 +100,4 @@ export class DataAFTTResultsProcessingService { this.logger.error(e.message); } } -} \ No newline at end of file +}