diff --git a/src/data/entity/m2m/opportunity-volunteer.ts b/src/data/entity/m2m/opportunity-volunteer.ts index bc6241e..115b54b 100644 --- a/src/data/entity/m2m/opportunity-volunteer.ts +++ b/src/data/entity/m2m/opportunity-volunteer.ts @@ -1,6 +1,7 @@ import { IsEnum } from "class-validator"; import { OpportunityVolunteerStatusType } from "need4deed-sdk"; import { + AfterInsert, AfterUpdate, Column, CreateDateColumn, @@ -59,9 +60,17 @@ export default class OpportunityVolunteer { @Column() volunteerId: number; + @AfterInsert() + async afterInsertHook() { + const { updateOpportunityMatching, updateVolunteerMatching } = await import("../../utils"); + updateOpportunityMatching(this.opportunityId); + updateVolunteerMatching(this.volunteerId); + } + @AfterUpdate() async afterUpdateHook() { - const { updateVolunteerMatching } = await import("../../utils"); + const { updateOpportunityMatching, updateVolunteerMatching } = await import("../../utils"); + updateOpportunityMatching(this.opportunityId); updateVolunteerMatching(this.volunteerId); } } diff --git a/src/data/entity/opportunity/opportunity.entity.ts b/src/data/entity/opportunity/opportunity.entity.ts index 6de9f29..d8bb919 100644 --- a/src/data/entity/opportunity/opportunity.entity.ts +++ b/src/data/entity/opportunity/opportunity.entity.ts @@ -3,6 +3,7 @@ import { OpportunityStatusType, OpportunityType, TranslatedIntoType, + VolunteerStateMatchType, } from "need4deed-sdk"; import { Column, @@ -51,6 +52,14 @@ export default class Opportunity { @IsEnum(OpportunityStatusType) status: OpportunityStatusType; + @Column({ + type: "enum", + enum: VolunteerStateMatchType, + default: VolunteerStateMatchType.NO_MATCHES, + }) + @IsEnum(VolunteerStateMatchType) + statusMatch: VolunteerStateMatchType; + @Column({ default: 1 }) @IsInt() numberVolunteers: number; diff --git a/src/data/lib/index.ts b/src/data/lib/index.ts index 31b7de9..43c7e86 100644 --- a/src/data/lib/index.ts +++ b/src/data/lib/index.ts @@ -1,3 +1,4 @@ export * from "./categorize"; +export * from "./resolve-opp-matching"; export * from "./revolve-vol-matching"; export * from "./snake-case"; diff --git a/src/data/lib/resolve-opp-matching.ts b/src/data/lib/resolve-opp-matching.ts new file mode 100644 index 0000000..6ac4de4 --- /dev/null +++ b/src/data/lib/resolve-opp-matching.ts @@ -0,0 +1,46 @@ +import { + OpportunityMatchStatusType, + OpportunityStatusType, + OpportunityVolunteerStatusType, +} from "need4deed-sdk"; + +export function resolveOpportunityMatchStatus( + volunteers: { status: OpportunityVolunteerStatusType }[], +): OpportunityMatchStatusType { + const hasMatched = volunteers.some( + (v) => + v.status === OpportunityVolunteerStatusType.MATCHED || + v.status === OpportunityVolunteerStatusType.ACTIVE, + ); + const hasPending = volunteers.some( + (v) => v.status === OpportunityVolunteerStatusType.PENDING, + ); + const hasPast = volunteers.some( + (v) => v.status === OpportunityVolunteerStatusType.PAST, + ); + + if (hasMatched) return OpportunityMatchStatusType.MATCHED; + if (hasPending) return OpportunityMatchStatusType.PENDING_MATCH; + if (hasPast) return OpportunityMatchStatusType.PAST; + return OpportunityMatchStatusType.NO_MATCHES; +} + +export function resolveOpportunityStatus( + volunteers: { status: OpportunityVolunteerStatusType }[], + currentStatus: OpportunityStatusType, +): OpportunityStatusType { + const hasActive = volunteers.some( + (v) => v.status === OpportunityVolunteerStatusType.ACTIVE, + ); + const hasPendingOrMatched = volunteers.some( + (v) => + v.status === OpportunityVolunteerStatusType.PENDING || + v.status === OpportunityVolunteerStatusType.MATCHED, + ); + + if (hasActive) return OpportunityStatusType.ACTIVE; + if (hasPendingOrMatched && currentStatus === OpportunityStatusType.NEW) { + return OpportunityStatusType.SEARCHING; + } + return currentStatus; +} diff --git a/src/data/migrations/1777284196924-add-opp-status-match.ts b/src/data/migrations/1777284196924-add-opp-status-match.ts new file mode 100644 index 0000000..c5623f5 --- /dev/null +++ b/src/data/migrations/1777284196924-add-opp-status-match.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddOppStatusMatch1777284196924 implements MigrationInterface { + name = "AddOppStatusMatch1777284196924"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."volunteer_status_match_enum" ADD VALUE IF NOT EXISTS 'vol-past'`, + ); + await queryRunner.query( + `ALTER TABLE "opportunity" ADD COLUMN IF NOT EXISTS "status_match" "volunteer_status_match_enum" NOT NULL DEFAULT 'vol-no-matches'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "opportunity" DROP COLUMN IF EXISTS "status_match"`, + ); + // PostgreSQL does not support removing enum values; vol-past remains in volunteer_status_match_enum. + } +} diff --git a/src/data/utils/index.ts b/src/data/utils/index.ts index 70c693a..911ab20 100644 --- a/src/data/utils/index.ts +++ b/src/data/utils/index.ts @@ -6,4 +6,5 @@ export * from "./getStartEndDates"; export * from "./passwd"; export * from "./refresh-materialized-view"; export * from "./remove-data"; +export * from "./update-opportunity-matching"; export * from "./update-volunteer-matching"; diff --git a/src/data/utils/update-opportunity-matching.ts b/src/data/utils/update-opportunity-matching.ts new file mode 100644 index 0000000..adab178 --- /dev/null +++ b/src/data/utils/update-opportunity-matching.ts @@ -0,0 +1,42 @@ +import logger from "../../logger"; +import { tryCatch } from "../../services/utils"; +import { dataSource } from "../data-source"; +import OpportunityVolunteer from "../entity/m2m/opportunity-volunteer"; +import Opportunity from "../entity/opportunity/opportunity.entity"; +import { resolveOpportunityMatchStatus, resolveOpportunityStatus } from "../lib"; +import { getRepository } from "./get-repository"; + +export async function updateOpportunityMatching(id: number): Promise { + const opportunityRepository = getRepository(dataSource, Opportunity); + const opportunity = await opportunityRepository.findOneBy({ id }); + if (!opportunity) { + logger.warn(`Opportunity id:${id} not found during match status update.`); + return; + } + + const opportunityVolunteerRepository = getRepository( + dataSource, + OpportunityVolunteer, + ); + const volunteersLinked = await opportunityVolunteerRepository.find({ + where: { opportunityId: id }, + }); + + const statusMatch = resolveOpportunityMatchStatus(volunteersLinked); + const status = resolveOpportunityStatus(volunteersLinked, opportunity.status); + + const changed = + statusMatch !== opportunity.statusMatch || status !== opportunity.status; + + if (changed) { + const [, error] = await tryCatch( + opportunityRepository.save( + Object.assign(opportunity, { statusMatch, status }), + ), + ); + + if (error) { + logger.warn(`During saving opportunity (id:${id}) occurred: ${error}`); + } + } +} diff --git a/src/services/dto/dto-opportunity.ts b/src/services/dto/dto-opportunity.ts index 8fcff81..3ecf653 100644 --- a/src/services/dto/dto-opportunity.ts +++ b/src/services/dto/dto-opportunity.ts @@ -4,6 +4,7 @@ import { ApiOpportunityGetList, ApiVolunteerOpportunityGetList, OpportunityType, + VolunteerStateMatchType, } from "need4deed-sdk"; import Comment from "../../data/entity/comment.entity"; import Opportunity from "../../data/entity/opportunity/opportunity.entity"; @@ -47,6 +48,7 @@ export function dtoOpportunityGetList( category: { id: opportunity.deal.profile.categoryId }, volunteerType: opportunity.type, statusOpportunity: opportunity.status, + statusMatch: opportunity.statusMatch ?? VolunteerStateMatchType.NO_MATCHES, createdAt: opportunity.createdAt, languages: opportunity.deal.profile.profileLanguage .filter(Boolean) @@ -108,6 +110,7 @@ export function dtoOpportunityGet( title: opportunityComments.title, volunteerType: opportunityComments.type, statusOpportunity: opportunityComments.status, + statusMatch: opportunityComments.statusMatch ?? VolunteerStateMatchType.NO_MATCHES, createdAt: opportunityComments.createdAt, category: { id: opportunityComments.deal.profile.categoryId }, description: getOpportunityDescription(opportunityComments),