diff --git a/backend/services/authorization-service/prisma/migrations/20250108000000_add_benefit_assessment_record/migration.sql b/backend/services/authorization-service/prisma/migrations/20250108000000_add_benefit_assessment_record/migration.sql new file mode 100644 index 00000000..4df1a5fd --- /dev/null +++ b/backend/services/authorization-service/prisma/migrations/20250108000000_add_benefit_assessment_record/migration.sql @@ -0,0 +1,44 @@ +-- CreateEnum +CREATE TYPE "BenefitActionType" AS ENUM ('ACTIVATED', 'RENEWED', 'DEACTIVATED', 'NO_CHANGE'); + +-- CreateTable +CREATE TABLE "benefit_assessment_records" ( + "id" TEXT NOT NULL, + "authorization_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "account_sequence" TEXT NOT NULL, + "role_type" "RoleType" NOT NULL, + "region_code" TEXT NOT NULL, + "region_name" TEXT NOT NULL, + "assessment_month" TEXT NOT NULL, + "month_index" INTEGER NOT NULL, + "monthly_target" INTEGER NOT NULL, + "cumulative_target" INTEGER NOT NULL, + "trees_completed" INTEGER NOT NULL, + "trees_required" INTEGER NOT NULL, + "benefit_action_taken" "BenefitActionType" NOT NULL, + "previous_benefit_status" BOOLEAN NOT NULL, + "new_benefit_status" BOOLEAN NOT NULL, + "new_valid_until" TIMESTAMP(3), + "result" "AssessmentResult" NOT NULL DEFAULT 'NOT_ASSESSED', + "remarks" VARCHAR(500), + "assessed_at" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "benefit_assessment_records_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "benefit_assessment_records_account_sequence_assessment_mont_idx" ON "benefit_assessment_records"("account_sequence", "assessment_month"); + +-- CreateIndex +CREATE INDEX "benefit_assessment_records_user_id_assessment_month_idx" ON "benefit_assessment_records"("user_id", "assessment_month"); + +-- CreateIndex +CREATE INDEX "benefit_assessment_records_role_type_region_code_assessment_idx" ON "benefit_assessment_records"("role_type", "region_code", "assessment_month"); + +-- CreateIndex +CREATE INDEX "benefit_assessment_records_assessment_month_result_idx" ON "benefit_assessment_records"("assessment_month", "result"); + +-- CreateIndex +CREATE UNIQUE INDEX "benefit_assessment_records_authorization_id_assessment_mont_key" ON "benefit_assessment_records"("authorization_id", "assessment_month"); diff --git a/backend/services/authorization-service/prisma/schema.prisma b/backend/services/authorization-service/prisma/schema.prisma index 1911711d..9573d802 100644 --- a/backend/services/authorization-service/prisma/schema.prisma +++ b/backend/services/authorization-service/prisma/schema.prisma @@ -483,6 +483,62 @@ model SystemAccountLedger { @@map("system_account_ledgers") } +// ============ 权益有效性考核记录表 ============ +// 专门记录权益激活/续期/失效的考核历史 +// 与 MonthlyAssessment (火柴人排名) 分离,避免职责混淆 +model BenefitAssessmentRecord { + id String @id @default(uuid()) + authorizationId String @map("authorization_id") + userId String @map("user_id") + accountSequence String @map("account_sequence") + roleType RoleType @map("role_type") + regionCode String @map("region_code") + regionName String @map("region_name") + + // 考核月份 + assessmentMonth String @map("assessment_month") // YYYY-MM + monthIndex Int @map("month_index") // 第几个月考核 + + // 考核目标 + monthlyTarget Int @map("monthly_target") // 当月目标 + cumulativeTarget Int @map("cumulative_target") // 累计目标 + + // 完成情况 + treesCompleted Int @map("trees_completed") // 实际完成数 + treesRequired Int @map("trees_required") // 需要达到的数量(用于续期判定) + + // 权益状态变化 + benefitActionTaken BenefitActionType @map("benefit_action_taken") // RENEWED / DEACTIVATED / ACTIVATED + previousBenefitStatus Boolean @map("previous_benefit_status") // 考核前权益状态 + newBenefitStatus Boolean @map("new_benefit_status") // 考核后权益状态 + newValidUntil DateTime? @map("new_valid_until") // 新的有效期截止日 + + // 考核结果 + result AssessmentResult @default(NOT_ASSESSED) + + // 备注 + remarks String? @map("remarks") @db.VarChar(500) + + // 时间戳 + assessedAt DateTime @map("assessed_at") + createdAt DateTime @default(now()) @map("created_at") + + @@unique([authorizationId, assessmentMonth]) + @@index([accountSequence, assessmentMonth]) + @@index([userId, assessmentMonth]) + @@index([roleType, regionCode, assessmentMonth]) + @@index([assessmentMonth, result]) + @@map("benefit_assessment_records") +} + +// ============ 权益操作类型枚举 ============ +enum BenefitActionType { + ACTIVATED // 首次激活 + RENEWED // 续期 + DEACTIVATED // 失效 + NO_CHANGE // 无变化(未到考核时间) +} + // ============================================ // Outbox 事件表 - 保证事件可靠发送 // 使用 Outbox Pattern 确保领域事件100%送达 diff --git a/backend/services/authorization-service/src/app.module.ts b/backend/services/authorization-service/src/app.module.ts index c14216b5..a348c712 100644 --- a/backend/services/authorization-service/src/app.module.ts +++ b/backend/services/authorization-service/src/app.module.ts @@ -23,6 +23,11 @@ import { SystemAccountRepositoryImpl, SYSTEM_ACCOUNT_REPOSITORY, } from '@/infrastructure/persistence/repositories/system-account.repository.impl' +// [2026-01-08] 新增:权益考核记录仓储,用于保存权益有效性考核历史 +import { + BenefitAssessmentRecordRepositoryImpl, + BENEFIT_ASSESSMENT_RECORD_REPOSITORY, +} from '@/infrastructure/persistence/repositories/benefit-assessment-record.repository.impl' import { RedisModule } from '@/infrastructure/redis/redis.module' import { KafkaModule } from '@/infrastructure/kafka/kafka.module' import { EventConsumerController } from '@/infrastructure/kafka/event-consumer.controller' @@ -100,6 +105,11 @@ const MockReferralRepository = { provide: SYSTEM_ACCOUNT_REPOSITORY, useClass: SystemAccountRepositoryImpl, }, + // [2026-01-08] 新增:权益考核记录仓储 + { + provide: BENEFIT_ASSESSMENT_RECORD_REPOSITORY, + useClass: BenefitAssessmentRecordRepositoryImpl, + }, MockReferralRepository, // External Service Clients (replaces mock) diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 6076a669..650f6eac 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, Logger } from '@nestjs/common' -import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' +import { AuthorizationRole, MonthlyAssessment, BenefitAssessmentRecord } from '@/domain/aggregates' import { LadderTargetRule } from '@/domain/entities' import { UserId, @@ -8,12 +8,14 @@ import { AuthorizationId, Month, } from '@/domain/value-objects' -import { RoleType, AuthorizationStatus } from '@/domain/enums' +import { RoleType, AuthorizationStatus, AssessmentResult, BenefitActionType } from '@/domain/enums' import { IAuthorizationRoleRepository, AUTHORIZATION_ROLE_REPOSITORY, IMonthlyAssessmentRepository, MONTHLY_ASSESSMENT_REPOSITORY, + IBenefitAssessmentRecordRepository, + BENEFIT_ASSESSMENT_RECORD_REPOSITORY, } from '@/domain/repositories' import { AuthorizationValidatorService, @@ -63,6 +65,9 @@ export class AuthorizationApplicationService { private readonly authorizationRepository: IAuthorizationRoleRepository, @Inject(MONTHLY_ASSESSMENT_REPOSITORY) private readonly assessmentRepository: IMonthlyAssessmentRepository, + // [2026-01-08] 新增:权益考核记录仓储,用于保存权益有效性考核历史 + @Inject(BENEFIT_ASSESSMENT_RECORD_REPOSITORY) + private readonly benefitAssessmentRecordRepository: IBenefitAssessmentRecordRepository, @Inject(REFERRAL_REPOSITORY) private readonly referralRepository: IReferralRepository, @Inject(TEAM_STATISTICS_REPOSITORY) @@ -1403,6 +1408,7 @@ export class AuthorizationApplicationService { for (const authCity of expiredAuthCities) { const accountSequence = authCity.userId.accountSequence + const AUTH_CITY_TARGET = 100 // 市团队授权固定目标:100棵 // 使用 getTreesForAssessment 获取正确的考核数据 const treesForAssessment = authCity.getTreesForAssessment(now) @@ -1412,13 +1418,38 @@ export class AuthorizationApplicationService { `treesForAssessment=${treesForAssessment}, ` + `monthlyTreesAdded=${authCity.monthlyTreesAdded}, ` + `lastMonthTreesAdded=${authCity.lastMonthTreesAdded}, ` + - `benefitValidUntil=${authCity.benefitValidUntil?.toISOString()}, target=100`, + `benefitValidUntil=${authCity.benefitValidUntil?.toISOString()}, target=${AUTH_CITY_TARGET}`, ) - if (treesForAssessment >= 100) { + // 计算考核月份:基于 benefitValidUntil + const assessmentMonth = authCity.benefitValidUntil + ? Month.fromDate(authCity.benefitValidUntil) + : Month.current().previous() + + const previousBenefitStatus = authCity.benefitActive + + if (treesForAssessment >= AUTH_CITY_TARGET) { // 达标,续期 authCity.renewBenefit(treesForAssessment) await this.authorizationRepository.save(authCity) + + // [2026-01-08] 保存权益考核记录到新表 + await this.saveBenefitAssessmentRecord({ + authorization: authCity, + assessmentMonth, + monthIndex: authCity.currentMonthIndex, + monthlyTarget: AUTH_CITY_TARGET, + cumulativeTarget: AUTH_CITY_TARGET, // 固定目标,无累计概念 + treesCompleted: treesForAssessment, + treesRequired: AUTH_CITY_TARGET, + benefitActionTaken: BenefitActionType.RENEWED, + previousBenefitStatus, + newBenefitStatus: true, + newValidUntil: authCity.benefitValidUntil, + result: AssessmentResult.PASS, + remarks: `续期成功:完成${treesForAssessment}棵,达到${AUTH_CITY_TARGET}棵目标`, + }) + renewedCount++ this.logger.log( @@ -1426,10 +1457,27 @@ export class AuthorizationApplicationService { `trees=${treesForAssessment}, new validUntil=${authCity.benefitValidUntil?.toISOString()}`, ) } else { + // [2026-01-08] 保存权益考核记录到新表(失效) + await this.saveBenefitAssessmentRecord({ + authorization: authCity, + assessmentMonth, + monthIndex: authCity.currentMonthIndex, + monthlyTarget: AUTH_CITY_TARGET, + cumulativeTarget: AUTH_CITY_TARGET, + treesCompleted: treesForAssessment, + treesRequired: AUTH_CITY_TARGET, + benefitActionTaken: BenefitActionType.DEACTIVATED, + previousBenefitStatus, + newBenefitStatus: false, + newValidUntil: null, + result: AssessmentResult.FAIL, + remarks: `考核不达标:完成${treesForAssessment}棵,未达到${AUTH_CITY_TARGET}棵目标`, + }) + // 不达标,级联停用 const result = await this.cascadeDeactivateAuthCityBenefits( accountSequence, - `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到100棵目标`, + `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到${AUTH_CITY_TARGET}棵目标`, ) deactivatedCount += result.deactivatedCount } @@ -1621,6 +1669,7 @@ export class AuthorizationApplicationService { for (const authProvince of expiredAuthProvinces) { const accountSequence = authProvince.userId.accountSequence + const AUTH_PROVINCE_TARGET = 500 // 省团队授权固定目标:500棵 // 使用 getTreesForAssessment 获取正确的考核数据 const treesForAssessment = authProvince.getTreesForAssessment(now) @@ -1630,13 +1679,38 @@ export class AuthorizationApplicationService { `treesForAssessment=${treesForAssessment}, ` + `monthlyTreesAdded=${authProvince.monthlyTreesAdded}, ` + `lastMonthTreesAdded=${authProvince.lastMonthTreesAdded}, ` + - `benefitValidUntil=${authProvince.benefitValidUntil?.toISOString()}, target=500`, + `benefitValidUntil=${authProvince.benefitValidUntil?.toISOString()}, target=${AUTH_PROVINCE_TARGET}`, ) - if (treesForAssessment >= 500) { + // 计算考核月份:基于 benefitValidUntil + const assessmentMonth = authProvince.benefitValidUntil + ? Month.fromDate(authProvince.benefitValidUntil) + : Month.current().previous() + + const previousBenefitStatus = authProvince.benefitActive + + if (treesForAssessment >= AUTH_PROVINCE_TARGET) { // 达标,续期 authProvince.renewBenefit(treesForAssessment) await this.authorizationRepository.save(authProvince) + + // [2026-01-08] 保存权益考核记录到新表 + await this.saveBenefitAssessmentRecord({ + authorization: authProvince, + assessmentMonth, + monthIndex: authProvince.currentMonthIndex, + monthlyTarget: AUTH_PROVINCE_TARGET, + cumulativeTarget: AUTH_PROVINCE_TARGET, // 固定目标,无累计概念 + treesCompleted: treesForAssessment, + treesRequired: AUTH_PROVINCE_TARGET, + benefitActionTaken: BenefitActionType.RENEWED, + previousBenefitStatus, + newBenefitStatus: true, + newValidUntil: authProvince.benefitValidUntil, + result: AssessmentResult.PASS, + remarks: `续期成功:完成${treesForAssessment}棵,达到${AUTH_PROVINCE_TARGET}棵目标`, + }) + renewedCount++ this.logger.log( @@ -1644,10 +1718,27 @@ export class AuthorizationApplicationService { `trees=${treesForAssessment}, new validUntil=${authProvince.benefitValidUntil?.toISOString()}`, ) } else { + // [2026-01-08] 保存权益考核记录到新表(失效) + await this.saveBenefitAssessmentRecord({ + authorization: authProvince, + assessmentMonth, + monthIndex: authProvince.currentMonthIndex, + monthlyTarget: AUTH_PROVINCE_TARGET, + cumulativeTarget: AUTH_PROVINCE_TARGET, + treesCompleted: treesForAssessment, + treesRequired: AUTH_PROVINCE_TARGET, + benefitActionTaken: BenefitActionType.DEACTIVATED, + previousBenefitStatus, + newBenefitStatus: false, + newValidUntil: null, + result: AssessmentResult.FAIL, + remarks: `考核不达标:完成${treesForAssessment}棵,未达到${AUTH_PROVINCE_TARGET}棵目标`, + }) + // 不达标,级联停用 const result = await this.cascadeDeactivateAuthProvinceBenefits( accountSequence, - `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到500棵目标`, + `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到${AUTH_PROVINCE_TARGET}棵目标`, ) deactivatedCount += result.deactivatedCount } @@ -1726,23 +1817,89 @@ export class AuthorizationApplicationService { `benefitValidUntil=${community.benefitValidUntil?.toISOString()}, target=10`, ) - if (treesForAssessment >= 10) { - // 达标,续期 - community.renewBenefit(treesForAssessment) - await this.authorizationRepository.save(community) - renewedCount++ + // 计算考核月份:基于 benefitValidUntil 而非当前时间 + // benefitValidUntil 是月末,考核的是该月 + const assessmentMonth = community.benefitValidUntil + ? Month.fromDate(community.benefitValidUntil) + : Month.current().previous() - this.logger.log( - `[processExpiredCommunityBenefits] Community ${accountSequence} renewed, ` + - `trees=${treesForAssessment}, new validUntil=${community.benefitValidUntil?.toISOString()}`, + const COMMUNITY_TARGET = 10 // 社区月度目标:10棵 + const previousBenefitStatus = community.benefitActive + + try { + if (treesForAssessment >= COMMUNITY_TARGET) { + // 先生成考核记录(达标)- 考核记录优先,更难人工补录 + await this.createCommunityAssessmentRecord( + community, + assessmentMonth, + treesForAssessment, + AssessmentResult.PASS, + ) + + // 再续期并保存 + community.renewBenefit(treesForAssessment) + await this.authorizationRepository.save(community) + + // [2026-01-08] 保存权益考核记录到新表 + await this.saveBenefitAssessmentRecord({ + authorization: community, + assessmentMonth, + monthIndex: community.currentMonthIndex, + monthlyTarget: COMMUNITY_TARGET, + cumulativeTarget: COMMUNITY_TARGET, + treesCompleted: treesForAssessment, + treesRequired: COMMUNITY_TARGET, + benefitActionTaken: BenefitActionType.RENEWED, + previousBenefitStatus, + newBenefitStatus: true, + newValidUntil: community.benefitValidUntil, + result: AssessmentResult.PASS, + remarks: `续期成功:完成${treesForAssessment}棵,达到${COMMUNITY_TARGET}棵目标`, + }) + + renewedCount++ + this.logger.log( + `[processExpiredCommunityBenefits] Community ${accountSequence} renewed, ` + + `trees=${treesForAssessment}, new validUntil=${community.benefitValidUntil?.toISOString()}`, + ) + } else { + // 先生成考核记录(不达标) + await this.createCommunityAssessmentRecord( + community, + assessmentMonth, + treesForAssessment, + AssessmentResult.FAIL, + ) + + // [2026-01-08] 保存权益考核记录到新表(失效) + await this.saveBenefitAssessmentRecord({ + authorization: community, + assessmentMonth, + monthIndex: community.currentMonthIndex, + monthlyTarget: COMMUNITY_TARGET, + cumulativeTarget: COMMUNITY_TARGET, + treesCompleted: treesForAssessment, + treesRequired: COMMUNITY_TARGET, + benefitActionTaken: BenefitActionType.DEACTIVATED, + previousBenefitStatus, + newBenefitStatus: false, + newValidUntil: null, + result: AssessmentResult.FAIL, + remarks: `考核不达标:完成${treesForAssessment}棵,未达到${COMMUNITY_TARGET}棵目标`, + }) + + // 再级联停用 + const result = await this.cascadeDeactivateCommunityBenefits( + accountSequence, + `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到10棵目标`, + ) + deactivatedCount += result.deactivatedCount + } + } catch (error) { + this.logger.error( + `[processExpiredCommunityBenefits] Failed to process community ${accountSequence}: ${error}`, ) - } else { - // 不达标,级联停用 - const result = await this.cascadeDeactivateCommunityBenefits( - accountSequence, - `月度考核不达标:考核期内新增${treesForAssessment}棵,未达到10棵目标`, - ) - deactivatedCount += result.deactivatedCount + // 继续处理下一个社区,不中断整体流程 } } @@ -1772,6 +1929,133 @@ export class AuthorizationApplicationService { .slice(0, limit) } + /** + * 创建社区权益考核记录 + * @param community 社区授权 + * @param assessmentMonth 考核月份 + * @param treesCompleted 完成的树数 + * @param result 考核结果 + */ + private async createCommunityAssessmentRecord( + community: AuthorizationRole, + assessmentMonth: Month, + treesCompleted: number, + result: AssessmentResult, + ): Promise { + const COMMUNITY_TARGET = 10 // 社区月度考核目标:10棵 + + // 检查是否已存在该月的考核记录,避免重复 + const existing = await this.assessmentRepository.findByAuthorizationAndMonth( + community.authorizationId, + assessmentMonth, + ) + if (existing) { + this.logger.warn( + `[createCommunityAssessmentRecord] Assessment record already exists for community ${community.userId.accountSequence}, ` + + `month=${assessmentMonth.value}, skipping`, + ) + return + } + + // 创建考核记录 + const assessment = MonthlyAssessment.create({ + authorizationId: community.authorizationId, + userId: community.userId, + roleType: RoleType.COMMUNITY, + regionCode: community.regionCode, + assessmentMonth, + monthIndex: community.currentMonthIndex, + monthlyTarget: COMMUNITY_TARGET, + cumulativeTarget: COMMUNITY_TARGET, // 社区无累计目标概念,使用月度目标 + }) + + // 执行考核(社区没有本地占比要求) + assessment.assess({ + cumulativeCompleted: treesCompleted, + localTeamCount: 0, + totalTeamCount: 0, + requireLocalPercentage: 0, + exemptFromPercentageCheck: true, + }) + + // 更新进度 + assessment.updateProgress(treesCompleted, treesCompleted) + + // 保存考核记录 + await this.assessmentRepository.save(assessment) + + this.logger.log( + `[createCommunityAssessmentRecord] Created assessment record for community ${community.userId.accountSequence}: ` + + `month=${assessmentMonth.value}, completed=${treesCompleted}, target=${COMMUNITY_TARGET}, result=${result}`, + ) + } + + /** + * 创建正式市/省公司考核记录 + * @param company 正式市/省公司授权 + * @param assessmentMonth 考核月份 + * @param monthIndex 当前月份索引 + * @param monthlyTarget 月度目标 + * @param cumulativeTarget 累计目标 + * @param treesCompleted 完成的树数 + * @param result 考核结果 + */ + private async createCompanyAssessmentRecord( + company: AuthorizationRole, + assessmentMonth: Month, + monthIndex: number, + monthlyTarget: number, + cumulativeTarget: number, + treesCompleted: number, + result: AssessmentResult, + ): Promise { + // 检查是否已存在该月的考核记录,避免重复 + const existing = await this.assessmentRepository.findByAuthorizationAndMonth( + company.authorizationId, + assessmentMonth, + ) + if (existing) { + this.logger.warn( + `[createCompanyAssessmentRecord] Assessment record already exists for ${company.roleType} ${company.userId.accountSequence}, ` + + `month=${assessmentMonth.value}, skipping`, + ) + return + } + + // 创建考核记录 + const assessment = MonthlyAssessment.create({ + authorizationId: company.authorizationId, + userId: company.userId, + roleType: company.roleType, + regionCode: company.regionCode, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + }) + + // 执行考核(正式市/省公司没有本地占比要求) + assessment.assess({ + cumulativeCompleted: treesCompleted, + localTeamCount: 0, + totalTeamCount: 0, + requireLocalPercentage: 0, + exemptFromPercentageCheck: true, + }) + + // 更新进度 + assessment.updateProgress(treesCompleted, treesCompleted) + + // 保存考核记录 + await this.assessmentRepository.save(assessment) + + this.logger.log( + `[createCompanyAssessmentRecord] Created assessment record for ${company.roleType} ${company.userId.accountSequence}: ` + + `month=${assessmentMonth.value}, monthIndex=${monthIndex}, completed=${treesCompleted}, ` + + `monthlyTarget=${monthlyTarget}, cumulativeTarget=${cumulativeTarget}, result=${result}`, + ) + } + /** * 获取社区权益分配方案 * 根据考核规则计算每棵树的社区权益应该分配给谁 @@ -2718,6 +3002,7 @@ export class AuthorizationApplicationService { const monthIndex = cityCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.CITY_COMPANY, monthIndex) const monthlyTarget = ladderTarget.monthlyTarget + const cumulativeTarget = ladderTarget.cumulativeTarget // 获取用于考核的树数 const treesForAssessment = cityCompany.getTreesForAssessment(now) @@ -2727,28 +3012,98 @@ export class AuthorizationApplicationService { `monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`, ) - if (treesForAssessment >= monthlyTarget) { - // 达标:续期权益并递增月份索引 - cityCompany.renewBenefit(treesForAssessment) - cityCompany.incrementMonthIndex() - await this.authorizationRepository.save(cityCompany) - renewedCount++ + // 计算考核月份:基于 benefitValidUntil + const assessmentMonth = cityCompany.benefitValidUntil + ? Month.fromDate(cityCompany.benefitValidUntil) + : Month.current().previous() - this.logger.log( - `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核达标,续期成功`, - ) - } else { - // 不达标:停用权益 - cityCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) - await this.authorizationRepository.save(cityCompany) + const previousBenefitStatus = cityCompany.benefitActive - await this.eventPublisher.publishAll(cityCompany.domainEvents) - cityCompany.clearDomainEvents() + try { + if (treesForAssessment >= monthlyTarget) { + // 先生成考核记录(达标) + await this.createCompanyAssessmentRecord( + cityCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesForAssessment, + AssessmentResult.PASS, + ) - deactivatedCount++ + // 达标:续期权益并递增月份索引 + cityCompany.renewBenefit(treesForAssessment) + cityCompany.incrementMonthIndex() + await this.authorizationRepository.save(cityCompany) - this.logger.log( - `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核不达标,权益已停用`, + // [2026-01-08] 保存权益考核记录到新表 + await this.saveBenefitAssessmentRecord({ + authorization: cityCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesCompleted: treesForAssessment, + treesRequired: monthlyTarget, + benefitActionTaken: BenefitActionType.RENEWED, + previousBenefitStatus, + newBenefitStatus: true, + newValidUntil: cityCompany.benefitValidUntil, + result: AssessmentResult.PASS, + remarks: `续期成功:完成${treesForAssessment}棵,达到第${monthIndex}月目标${monthlyTarget}棵`, + }) + + renewedCount++ + + this.logger.log( + `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核达标,续期成功`, + ) + } else { + // 先生成考核记录(不达标) + await this.createCompanyAssessmentRecord( + cityCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesForAssessment, + AssessmentResult.FAIL, + ) + + // [2026-01-08] 保存权益考核记录到新表(失效) + await this.saveBenefitAssessmentRecord({ + authorization: cityCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesCompleted: treesForAssessment, + treesRequired: monthlyTarget, + benefitActionTaken: BenefitActionType.DEACTIVATED, + previousBenefitStatus, + newBenefitStatus: false, + newValidUntil: null, + result: AssessmentResult.FAIL, + remarks: `考核不达标:完成${treesForAssessment}棵,未达到第${monthIndex}月目标${monthlyTarget}棵`, + }) + + // 不达标:停用权益 + cityCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) + await this.authorizationRepository.save(cityCompany) + + await this.eventPublisher.publishAll(cityCompany.domainEvents) + cityCompany.clearDomainEvents() + + deactivatedCount++ + + this.logger.log( + `[processExpiredCityCompanyBenefits] ${cityCompany.userId.accountSequence} 考核不达标,权益已停用`, + ) + } + } catch (error) { + this.logger.error( + `[processExpiredCityCompanyBenefits] Failed to process ${cityCompany.userId.accountSequence}: ${error}`, ) } } @@ -2801,6 +3156,7 @@ export class AuthorizationApplicationService { const monthIndex = provinceCompany.currentMonthIndex || 1 const ladderTarget = LadderTargetRule.getTarget(RoleType.PROVINCE_COMPANY, monthIndex) const monthlyTarget = ladderTarget.monthlyTarget + const cumulativeTarget = ladderTarget.cumulativeTarget // 获取用于考核的树数 const treesForAssessment = provinceCompany.getTreesForAssessment(now) @@ -2810,28 +3166,98 @@ export class AuthorizationApplicationService { `monthIndex=${monthIndex}, target=${monthlyTarget}, trees=${treesForAssessment}`, ) - if (treesForAssessment >= monthlyTarget) { - // 达标:续期权益并递增月份索引 - provinceCompany.renewBenefit(treesForAssessment) - provinceCompany.incrementMonthIndex() - await this.authorizationRepository.save(provinceCompany) - renewedCount++ + // 计算考核月份:基于 benefitValidUntil + const assessmentMonth = provinceCompany.benefitValidUntil + ? Month.fromDate(provinceCompany.benefitValidUntil) + : Month.current().previous() - this.logger.log( - `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核达标,续期成功`, - ) - } else { - // 不达标:停用权益 - provinceCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) - await this.authorizationRepository.save(provinceCompany) + const previousBenefitStatus = provinceCompany.benefitActive - await this.eventPublisher.publishAll(provinceCompany.domainEvents) - provinceCompany.clearDomainEvents() + try { + if (treesForAssessment >= monthlyTarget) { + // 先生成考核记录(达标) + await this.createCompanyAssessmentRecord( + provinceCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesForAssessment, + AssessmentResult.PASS, + ) - deactivatedCount++ + // 达标:续期权益并递增月份索引 + provinceCompany.renewBenefit(treesForAssessment) + provinceCompany.incrementMonthIndex() + await this.authorizationRepository.save(provinceCompany) - this.logger.log( - `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核不达标,权益已停用`, + // [2026-01-08] 保存权益考核记录到新表 + await this.saveBenefitAssessmentRecord({ + authorization: provinceCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesCompleted: treesForAssessment, + treesRequired: monthlyTarget, + benefitActionTaken: BenefitActionType.RENEWED, + previousBenefitStatus, + newBenefitStatus: true, + newValidUntil: provinceCompany.benefitValidUntil, + result: AssessmentResult.PASS, + remarks: `续期成功:完成${treesForAssessment}棵,达到第${monthIndex}月目标${monthlyTarget}棵`, + }) + + renewedCount++ + + this.logger.log( + `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核达标,续期成功`, + ) + } else { + // 先生成考核记录(不达标) + await this.createCompanyAssessmentRecord( + provinceCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesForAssessment, + AssessmentResult.FAIL, + ) + + // [2026-01-08] 保存权益考核记录到新表(失效) + await this.saveBenefitAssessmentRecord({ + authorization: provinceCompany, + assessmentMonth, + monthIndex, + monthlyTarget, + cumulativeTarget, + treesCompleted: treesForAssessment, + treesRequired: monthlyTarget, + benefitActionTaken: BenefitActionType.DEACTIVATED, + previousBenefitStatus, + newBenefitStatus: false, + newValidUntil: null, + result: AssessmentResult.FAIL, + remarks: `考核不达标:完成${treesForAssessment}棵,未达到第${monthIndex}月目标${monthlyTarget}棵`, + }) + + // 不达标:停用权益 + provinceCompany.deactivateBenefit(`月度考核不达标(${treesForAssessment}/${monthlyTarget})`) + await this.authorizationRepository.save(provinceCompany) + + await this.eventPublisher.publishAll(provinceCompany.domainEvents) + provinceCompany.clearDomainEvents() + + deactivatedCount++ + + this.logger.log( + `[processExpiredProvinceCompanyBenefits] ${provinceCompany.userId.accountSequence} 考核不达标,权益已停用`, + ) + } + } catch (error) { + this.logger.error( + `[processExpiredProvinceCompanyBenefits] Failed to process ${provinceCompany.userId.accountSequence}: ${error}`, ) } } @@ -3520,4 +3946,86 @@ export class AuthorizationApplicationService { return { items, total, page, limit } } + + // ============================================================================ + // [2026-01-08] 新增:权益有效性考核记录 + // 保存到独立的 BenefitAssessmentRecord 表,与火柴人排名(MonthlyAssessment)分离 + // ============================================================================ + + /** + * 创建权益有效性考核记录(保存到新表 BenefitAssessmentRecord) + * @param authorization 授权角色 + * @param assessmentMonth 考核月份 + * @param monthIndex 当前月份索引 + * @param monthlyTarget 月度目标 + * @param cumulativeTarget 累计目标 + * @param treesCompleted 实际完成数 + * @param treesRequired 需要达到的数量 + * @param benefitActionTaken 权益操作类型 + * @param previousBenefitStatus 考核前权益状态 + * @param newBenefitStatus 考核后权益状态 + * @param newValidUntil 新的有效期截止日 + * @param result 考核结果 + * @param remarks 备注 + */ + private async saveBenefitAssessmentRecord(params: { + authorization: AuthorizationRole + assessmentMonth: Month + monthIndex: number + monthlyTarget: number + cumulativeTarget: number + treesCompleted: number + treesRequired: number + benefitActionTaken: BenefitActionType + previousBenefitStatus: boolean + newBenefitStatus: boolean + newValidUntil: Date | null + result: AssessmentResult + remarks?: string + }): Promise { + const { authorization, assessmentMonth } = params + + // 检查是否已存在该月的记录,避免重复 + const existing = await this.benefitAssessmentRecordRepository.findByAuthorizationAndMonth( + authorization.authorizationId, + assessmentMonth, + ) + if (existing) { + this.logger.warn( + `[saveBenefitAssessmentRecord] Record already exists for ${authorization.roleType} ` + + `${authorization.userId.accountSequence}, month=${assessmentMonth.value}, skipping`, + ) + return + } + + // 创建权益考核记录 + const record = BenefitAssessmentRecord.create({ + authorizationId: authorization.authorizationId, + userId: authorization.userId, + roleType: authorization.roleType, + regionCode: authorization.regionCode, + regionName: authorization.regionName, + assessmentMonth, + monthIndex: params.monthIndex, + monthlyTarget: params.monthlyTarget, + cumulativeTarget: params.cumulativeTarget, + treesCompleted: params.treesCompleted, + treesRequired: params.treesRequired, + benefitActionTaken: params.benefitActionTaken, + previousBenefitStatus: params.previousBenefitStatus, + newBenefitStatus: params.newBenefitStatus, + newValidUntil: params.newValidUntil, + result: params.result, + remarks: params.remarks, + }) + + // 保存到新表 + await this.benefitAssessmentRecordRepository.save(record) + + this.logger.log( + `[saveBenefitAssessmentRecord] Created record for ${authorization.roleType} ` + + `${authorization.userId.accountSequence}: month=${assessmentMonth.value}, ` + + `action=${params.benefitActionTaken}, result=${params.result}`, + ) + } } diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts index da195fab..ac2c9aa6 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts @@ -942,8 +942,13 @@ export class AuthorizationRole extends AggregateRoot { /** * 续期权益(月度考核达标后) * 延长有效期到下下月末 + * + * 注意:不修改 monthlyTreesAdded,因为: + * - treesAdded 是上月的考核完成数(来自 lastMonthTreesAdded) + * - monthlyTreesAdded 是当月累计器,已被月初存档重置为 0 + * - 当月新增的树会通过 addMonthlyTrees() 累加 */ - renewBenefit(treesAdded: number): void { + renewBenefit(_treesAdded: number): void { if (!this._benefitActive) { throw new DomainError('权益未激活,无法续期') } @@ -951,7 +956,7 @@ export class AuthorizationRole extends AggregateRoot { const now = new Date() this._benefitValidUntil = AuthorizationRole.calculateBenefitValidUntil(now) this._lastAssessmentMonth = AuthorizationRole.getCurrentMonthString(now) - this._monthlyTreesAdded = treesAdded + this._currentMonthIndex += 1 // 考核月份索引递增 this._updatedAt = now } diff --git a/backend/services/authorization-service/src/domain/aggregates/benefit-assessment-record.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/benefit-assessment-record.aggregate.ts new file mode 100644 index 00000000..8e2d7d3f --- /dev/null +++ b/backend/services/authorization-service/src/domain/aggregates/benefit-assessment-record.aggregate.ts @@ -0,0 +1,234 @@ +import { AggregateRoot } from './aggregate-root.base' +import { AuthorizationId, UserId, RegionCode, Month } from '@/domain/value-objects' +import { RoleType, AssessmentResult, BenefitActionType } from '@/domain/enums' +import { v4 as uuidv4 } from 'uuid' + +export interface BenefitAssessmentRecordProps { + id: string + authorizationId: AuthorizationId + userId: UserId + roleType: RoleType + regionCode: RegionCode + regionName: string + assessmentMonth: Month + monthIndex: number + monthlyTarget: number + cumulativeTarget: number + treesCompleted: number + treesRequired: number + benefitActionTaken: BenefitActionType + previousBenefitStatus: boolean + newBenefitStatus: boolean + newValidUntil: Date | null + result: AssessmentResult + remarks: string | null + assessedAt: Date + createdAt: Date +} + +/** + * 权益有效性考核记录 + * 专门记录权益激活/续期/失效的考核历史 + * 与 MonthlyAssessment (火柴人排名) 分离,避免职责混淆 + */ +export class BenefitAssessmentRecord extends AggregateRoot { + private _id: string + private _authorizationId: AuthorizationId + private _userId: UserId + private _roleType: RoleType + private _regionCode: RegionCode + private _regionName: string + + // 考核月份 + private _assessmentMonth: Month + private _monthIndex: number + + // 考核目标 + private _monthlyTarget: number + private _cumulativeTarget: number + + // 完成情况 + private _treesCompleted: number + private _treesRequired: number + + // 权益状态变化 + private _benefitActionTaken: BenefitActionType + private _previousBenefitStatus: boolean + private _newBenefitStatus: boolean + private _newValidUntil: Date | null + + // 考核结果 + private _result: AssessmentResult + + // 备注 + private _remarks: string | null + + // 时间戳 + private _assessedAt: Date + private _createdAt: Date + + // Getters + get id(): string { + return this._id + } + get authorizationId(): AuthorizationId { + return this._authorizationId + } + get userId(): UserId { + return this._userId + } + get roleType(): RoleType { + return this._roleType + } + get regionCode(): RegionCode { + return this._regionCode + } + get regionName(): string { + return this._regionName + } + get assessmentMonth(): Month { + return this._assessmentMonth + } + get monthIndex(): number { + return this._monthIndex + } + get monthlyTarget(): number { + return this._monthlyTarget + } + get cumulativeTarget(): number { + return this._cumulativeTarget + } + get treesCompleted(): number { + return this._treesCompleted + } + get treesRequired(): number { + return this._treesRequired + } + get benefitActionTaken(): BenefitActionType { + return this._benefitActionTaken + } + get previousBenefitStatus(): boolean { + return this._previousBenefitStatus + } + get newBenefitStatus(): boolean { + return this._newBenefitStatus + } + get newValidUntil(): Date | null { + return this._newValidUntil + } + get result(): AssessmentResult { + return this._result + } + get remarks(): string | null { + return this._remarks + } + get assessedAt(): Date { + return this._assessedAt + } + get createdAt(): Date { + return this._createdAt + } + + // 私有构造函数 + private constructor(props: BenefitAssessmentRecordProps) { + super() + this._id = props.id + this._authorizationId = props.authorizationId + this._userId = props.userId + this._roleType = props.roleType + this._regionCode = props.regionCode + this._regionName = props.regionName + this._assessmentMonth = props.assessmentMonth + this._monthIndex = props.monthIndex + this._monthlyTarget = props.monthlyTarget + this._cumulativeTarget = props.cumulativeTarget + this._treesCompleted = props.treesCompleted + this._treesRequired = props.treesRequired + this._benefitActionTaken = props.benefitActionTaken + this._previousBenefitStatus = props.previousBenefitStatus + this._newBenefitStatus = props.newBenefitStatus + this._newValidUntil = props.newValidUntil + this._result = props.result + this._remarks = props.remarks + this._assessedAt = props.assessedAt + this._createdAt = props.createdAt + } + + // 工厂方法 - 从数据库重建 + static fromPersistence(props: BenefitAssessmentRecordProps): BenefitAssessmentRecord { + return new BenefitAssessmentRecord(props) + } + + // 工厂方法 - 创建新记录 + static create(params: { + authorizationId: AuthorizationId + userId: UserId + roleType: RoleType + regionCode: RegionCode + regionName: string + assessmentMonth: Month + monthIndex: number + monthlyTarget: number + cumulativeTarget: number + treesCompleted: number + treesRequired: number + benefitActionTaken: BenefitActionType + previousBenefitStatus: boolean + newBenefitStatus: boolean + newValidUntil: Date | null + result: AssessmentResult + remarks?: string | null + }): BenefitAssessmentRecord { + return new BenefitAssessmentRecord({ + id: uuidv4(), + authorizationId: params.authorizationId, + userId: params.userId, + roleType: params.roleType, + regionCode: params.regionCode, + regionName: params.regionName, + assessmentMonth: params.assessmentMonth, + monthIndex: params.monthIndex, + monthlyTarget: params.monthlyTarget, + cumulativeTarget: params.cumulativeTarget, + treesCompleted: params.treesCompleted, + treesRequired: params.treesRequired, + benefitActionTaken: params.benefitActionTaken, + previousBenefitStatus: params.previousBenefitStatus, + newBenefitStatus: params.newBenefitStatus, + newValidUntil: params.newValidUntil, + result: params.result, + remarks: params.remarks ?? null, + assessedAt: new Date(), + createdAt: new Date(), + }) + } + + /** + * 转换为持久化数据 + */ + toPersistence(): Record { + return { + id: this._id, + authorizationId: this._authorizationId.value, + userId: this._userId.value, + accountSequence: this._userId.accountSequence, + roleType: this._roleType, + regionCode: this._regionCode.value, + regionName: this._regionName, + assessmentMonth: this._assessmentMonth.value, + monthIndex: this._monthIndex, + monthlyTarget: this._monthlyTarget, + cumulativeTarget: this._cumulativeTarget, + treesCompleted: this._treesCompleted, + treesRequired: this._treesRequired, + benefitActionTaken: this._benefitActionTaken, + previousBenefitStatus: this._previousBenefitStatus, + newBenefitStatus: this._newBenefitStatus, + newValidUntil: this._newValidUntil, + result: this._result, + remarks: this._remarks, + assessedAt: this._assessedAt, + createdAt: this._createdAt, + } + } +} diff --git a/backend/services/authorization-service/src/domain/aggregates/index.ts b/backend/services/authorization-service/src/domain/aggregates/index.ts index c5857467..56039340 100644 --- a/backend/services/authorization-service/src/domain/aggregates/index.ts +++ b/backend/services/authorization-service/src/domain/aggregates/index.ts @@ -2,3 +2,4 @@ export * from './aggregate-root.base' export * from './authorization-role.aggregate' export * from './monthly-assessment.aggregate' export * from './system-account.aggregate' +export * from './benefit-assessment-record.aggregate' diff --git a/backend/services/authorization-service/src/domain/enums/index.ts b/backend/services/authorization-service/src/domain/enums/index.ts index 772e73a2..405ff0b3 100644 --- a/backend/services/authorization-service/src/domain/enums/index.ts +++ b/backend/services/authorization-service/src/domain/enums/index.ts @@ -74,3 +74,11 @@ export enum SystemAccountStatus { ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', } + +// 权益操作类型 +export enum BenefitActionType { + ACTIVATED = 'ACTIVATED', // 首次激活 + RENEWED = 'RENEWED', // 续期 + DEACTIVATED = 'DEACTIVATED', // 失效 + NO_CHANGE = 'NO_CHANGE', // 无变化 +} diff --git a/backend/services/authorization-service/src/domain/repositories/benefit-assessment-record.repository.ts b/backend/services/authorization-service/src/domain/repositories/benefit-assessment-record.repository.ts new file mode 100644 index 00000000..1a5f2185 --- /dev/null +++ b/backend/services/authorization-service/src/domain/repositories/benefit-assessment-record.repository.ts @@ -0,0 +1,27 @@ +import { BenefitAssessmentRecord } from '@/domain/aggregates' +import { AuthorizationId, UserId, Month, RegionCode } from '@/domain/value-objects' +import { RoleType } from '@/domain/enums' + +export const BENEFIT_ASSESSMENT_RECORD_REPOSITORY = Symbol('IBenefitAssessmentRecordRepository') + +export interface IBenefitAssessmentRecordRepository { + save(record: BenefitAssessmentRecord): Promise + saveAll(records: BenefitAssessmentRecord[]): Promise + findById(id: string): Promise + findByAuthorizationAndMonth( + authorizationId: AuthorizationId, + month: Month, + ): Promise + findByUserAndMonth(userId: UserId, month: Month): Promise + findByMonthAndRoleType( + month: Month, + roleType: RoleType, + ): Promise + findByMonthAndRegion( + month: Month, + roleType: RoleType, + regionCode: RegionCode, + ): Promise + findByAuthorization(authorizationId: AuthorizationId): Promise + delete(id: string): Promise +} diff --git a/backend/services/authorization-service/src/domain/repositories/index.ts b/backend/services/authorization-service/src/domain/repositories/index.ts index 8d505fbf..9508938f 100644 --- a/backend/services/authorization-service/src/domain/repositories/index.ts +++ b/backend/services/authorization-service/src/domain/repositories/index.ts @@ -2,3 +2,4 @@ export * from './authorization-role.repository' export * from './monthly-assessment.repository' export * from './planting-restriction.repository' export * from './system-account.repository' +export * from './benefit-assessment-record.repository' diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/benefit-assessment-record.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/benefit-assessment-record.repository.impl.ts new file mode 100644 index 00000000..b7200c04 --- /dev/null +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/benefit-assessment-record.repository.impl.ts @@ -0,0 +1,210 @@ +import { Injectable } from '@nestjs/common' +import { PrismaService } from '../prisma/prisma.service' +import { + IBenefitAssessmentRecordRepository, + BENEFIT_ASSESSMENT_RECORD_REPOSITORY, +} from '@/domain/repositories' +import { BenefitAssessmentRecord, BenefitAssessmentRecordProps } from '@/domain/aggregates' +import { AuthorizationId, UserId, RegionCode, Month } from '@/domain/value-objects' +import { RoleType, AssessmentResult, BenefitActionType } from '@/domain/enums' + +@Injectable() +export class BenefitAssessmentRecordRepositoryImpl implements IBenefitAssessmentRecordRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(record: BenefitAssessmentRecord): Promise { + const data = record.toPersistence() + await this.prisma.benefitAssessmentRecord.upsert({ + where: { + authorizationId_assessmentMonth: { + authorizationId: data.authorizationId, + assessmentMonth: data.assessmentMonth, + }, + }, + create: { + id: data.id, + authorizationId: data.authorizationId, + userId: data.userId, + accountSequence: data.accountSequence, + roleType: data.roleType, + regionCode: data.regionCode, + regionName: data.regionName, + assessmentMonth: data.assessmentMonth, + monthIndex: data.monthIndex, + monthlyTarget: data.monthlyTarget, + cumulativeTarget: data.cumulativeTarget, + treesCompleted: data.treesCompleted, + treesRequired: data.treesRequired, + benefitActionTaken: data.benefitActionTaken, + previousBenefitStatus: data.previousBenefitStatus, + newBenefitStatus: data.newBenefitStatus, + newValidUntil: data.newValidUntil, + result: data.result, + remarks: data.remarks, + assessedAt: data.assessedAt, + }, + update: { + treesCompleted: data.treesCompleted, + treesRequired: data.treesRequired, + benefitActionTaken: data.benefitActionTaken, + previousBenefitStatus: data.previousBenefitStatus, + newBenefitStatus: data.newBenefitStatus, + newValidUntil: data.newValidUntil, + result: data.result, + remarks: data.remarks, + assessedAt: data.assessedAt, + }, + }) + } + + async saveAll(records: BenefitAssessmentRecord[]): Promise { + await this.prisma.$transaction( + records.map((record) => { + const data = record.toPersistence() + return this.prisma.benefitAssessmentRecord.upsert({ + where: { + authorizationId_assessmentMonth: { + authorizationId: data.authorizationId, + assessmentMonth: data.assessmentMonth, + }, + }, + create: { + id: data.id, + authorizationId: data.authorizationId, + userId: data.userId, + accountSequence: data.accountSequence, + roleType: data.roleType, + regionCode: data.regionCode, + regionName: data.regionName, + assessmentMonth: data.assessmentMonth, + monthIndex: data.monthIndex, + monthlyTarget: data.monthlyTarget, + cumulativeTarget: data.cumulativeTarget, + treesCompleted: data.treesCompleted, + treesRequired: data.treesRequired, + benefitActionTaken: data.benefitActionTaken, + previousBenefitStatus: data.previousBenefitStatus, + newBenefitStatus: data.newBenefitStatus, + newValidUntil: data.newValidUntil, + result: data.result, + remarks: data.remarks, + assessedAt: data.assessedAt, + }, + update: { + treesCompleted: data.treesCompleted, + treesRequired: data.treesRequired, + benefitActionTaken: data.benefitActionTaken, + previousBenefitStatus: data.previousBenefitStatus, + newBenefitStatus: data.newBenefitStatus, + newValidUntil: data.newValidUntil, + result: data.result, + remarks: data.remarks, + assessedAt: data.assessedAt, + }, + }) + }), + ) + } + + async findById(id: string): Promise { + const record = await this.prisma.benefitAssessmentRecord.findUnique({ + where: { id }, + }) + return record ? this.toDomain(record) : null + } + + async findByAuthorizationAndMonth( + authorizationId: AuthorizationId, + month: Month, + ): Promise { + const record = await this.prisma.benefitAssessmentRecord.findFirst({ + where: { + authorizationId: authorizationId.value, + assessmentMonth: month.value, + }, + }) + return record ? this.toDomain(record) : null + } + + async findByUserAndMonth(userId: UserId, month: Month): Promise { + const records = await this.prisma.benefitAssessmentRecord.findMany({ + where: { + userId: userId.value, + assessmentMonth: month.value, + }, + }) + return records.map((record) => this.toDomain(record)) + } + + async findByMonthAndRoleType( + month: Month, + roleType: RoleType, + ): Promise { + const records = await this.prisma.benefitAssessmentRecord.findMany({ + where: { + assessmentMonth: month.value, + roleType: roleType, + }, + orderBy: { createdAt: 'desc' }, + }) + return records.map((record) => this.toDomain(record)) + } + + async findByMonthAndRegion( + month: Month, + roleType: RoleType, + regionCode: RegionCode, + ): Promise { + const records = await this.prisma.benefitAssessmentRecord.findMany({ + where: { + assessmentMonth: month.value, + roleType: roleType, + regionCode: regionCode.value, + }, + orderBy: { createdAt: 'desc' }, + }) + return records.map((record) => this.toDomain(record)) + } + + async findByAuthorization(authorizationId: AuthorizationId): Promise { + const records = await this.prisma.benefitAssessmentRecord.findMany({ + where: { authorizationId: authorizationId.value }, + orderBy: { monthIndex: 'asc' }, + }) + return records.map((record) => this.toDomain(record)) + } + + async delete(id: string): Promise { + await this.prisma.benefitAssessmentRecord.delete({ + where: { id }, + }) + } + + private toDomain(record: any): BenefitAssessmentRecord { + const props: BenefitAssessmentRecordProps = { + id: record.id, + authorizationId: AuthorizationId.create(record.authorizationId), + userId: UserId.create(record.userId, record.accountSequence), + roleType: record.roleType as RoleType, + regionCode: RegionCode.create(record.regionCode), + regionName: record.regionName, + assessmentMonth: Month.create(record.assessmentMonth), + monthIndex: record.monthIndex, + monthlyTarget: record.monthlyTarget, + cumulativeTarget: record.cumulativeTarget, + treesCompleted: record.treesCompleted, + treesRequired: record.treesRequired, + benefitActionTaken: record.benefitActionTaken as BenefitActionType, + previousBenefitStatus: record.previousBenefitStatus, + newBenefitStatus: record.newBenefitStatus, + newValidUntil: record.newValidUntil, + result: record.result as AssessmentResult, + remarks: record.remarks, + assessedAt: record.assessedAt, + createdAt: record.createdAt, + } + return BenefitAssessmentRecord.fromPersistence(props) + } +} + +export { BENEFIT_ASSESSMENT_RECORD_REPOSITORY } diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/index.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/index.ts index 21925b7b..95764556 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/index.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/index.ts @@ -1,3 +1,4 @@ export * from './authorization-role.repository.impl' export * from './monthly-assessment.repository.impl' export * from './system-account.repository.impl' +export * from './benefit-assessment-record.repository.impl'