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 4255f5bc..da195fab 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 @@ -675,6 +675,106 @@ export class AuthorizationRole extends AggregateRoot { return auth } + // 工厂方法 - 自动升级创建省区域(省团队伞下第一个累计达到5万棵) + // 与手动授权 createProvinceCompany 完全一致,仅 authorizedBy 为 null + static createAutoUpgradedProvinceCompany(params: { + userId: UserId + provinceCode: string + provinceName: string + }): AuthorizationRole { + const now = new Date() + const auth = new AuthorizationRole({ + authorizationId: AuthorizationId.generate(), + userId: params.userId, + roleType: RoleType.PROVINCE_COMPANY, + regionCode: RegionCode.create(params.provinceCode), + regionName: params.provinceName, + status: AuthorizationStatus.AUTHORIZED, + displayTitle: params.provinceName, + authorizedAt: now, + authorizedBy: null, // 自动升级无管理员 + revokedAt: null, + revokedBy: null, + revokeReason: null, + assessmentConfig: AssessmentConfig.forProvince(), + requireLocalPercentage: 0, + exemptFromPercentageCheck: true, + benefitActive: true, // 授权即激活(与手动授权一致) + benefitActivatedAt: now, + benefitDeactivatedAt: null, + benefitValidUntil: AuthorizationRole.calculateBenefitValidUntil(now), + lastAssessmentMonth: AuthorizationRole.getCurrentMonthString(now), + monthlyTreesAdded: 0, + lastMonthTreesAdded: 0, + currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致) + deletedAt: null, + createdAt: now, + updatedAt: now, + }) + + auth.addDomainEvent( + new ProvinceCompanyAuthorizedEvent({ + authorizationId: auth.authorizationId.value, + userId: params.userId.value, + provinceCode: params.provinceCode, + provinceName: params.provinceName, + authorizedBy: null, // 自动升级 + }), + ) + + return auth + } + + // 工厂方法 - 自动升级创建市区域(市团队伞下第一个累计达到1万棵) + // 与手动授权 createCityCompany 完全一致,仅 authorizedBy 为 null + static createAutoUpgradedCityCompany(params: { + userId: UserId + cityCode: string + cityName: string + }): AuthorizationRole { + const now = new Date() + const auth = new AuthorizationRole({ + authorizationId: AuthorizationId.generate(), + userId: params.userId, + roleType: RoleType.CITY_COMPANY, + regionCode: RegionCode.create(params.cityCode), + regionName: params.cityName, + status: AuthorizationStatus.AUTHORIZED, + displayTitle: params.cityName, + authorizedAt: now, + authorizedBy: null, // 自动升级无管理员 + revokedAt: null, + revokedBy: null, + revokeReason: null, + assessmentConfig: AssessmentConfig.forCity(), + requireLocalPercentage: 0, + exemptFromPercentageCheck: true, + benefitActive: true, // 授权即激活(与手动授权一致) + benefitActivatedAt: now, + benefitDeactivatedAt: null, + benefitValidUntil: AuthorizationRole.calculateBenefitValidUntil(now), + lastAssessmentMonth: AuthorizationRole.getCurrentMonthString(now), + monthlyTreesAdded: 0, + lastMonthTreesAdded: 0, + currentMonthIndex: 1, // 从第1个月开始考核(与手动授权一致) + deletedAt: null, + createdAt: now, + updatedAt: now, + }) + + auth.addDomainEvent( + new CityCompanyAuthorizedEvent({ + authorizationId: auth.authorizationId.value, + userId: params.userId.value, + cityCode: params.cityCode, + cityName: params.cityName, + authorizedBy: null, // 自动升级 + }), + ) + + return auth + } + // 工厂方法 - Admin授权省团队 static createAuthProvinceCompanyByAdmin(params: { userId: UserId diff --git a/backend/services/authorization-service/src/domain/events/authorization-events.ts b/backend/services/authorization-service/src/domain/events/authorization-events.ts index 80789378..fda48055 100644 --- a/backend/services/authorization-service/src/domain/events/authorization-events.ts +++ b/backend/services/authorization-service/src/domain/events/authorization-events.ts @@ -96,7 +96,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent { userId: string provinceCode: string provinceName: string - authorizedBy: string + authorizedBy: string | null // null 表示自动升级 } constructor(data: { @@ -104,7 +104,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent { userId: string provinceCode: string provinceName: string - authorizedBy: string + authorizedBy: string | null }) { super() this.aggregateId = data.authorizationId @@ -121,7 +121,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent { userId: string cityCode: string cityName: string - authorizedBy: string + authorizedBy: string | null // null 表示自动升级 } constructor(data: { @@ -129,7 +129,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent { userId: string cityCode: string cityName: string - authorizedBy: string + authorizedBy: string | null }) { super() this.aggregateId = data.authorizationId diff --git a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts index acc2532e..b878367c 100644 --- a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts +++ b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts @@ -1,152 +1,154 @@ -import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' -import { LadderTargetRule } from '@/domain/entities' -import { Month, RegionCode } from '@/domain/value-objects' -import { RoleType } from '@/domain/enums' -import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/domain/repositories' - -export interface TeamStatistics { - userId: string - accountSequence: string - totalTeamPlantingCount: number - selfPlantingCount: number - /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ - get subordinateTeamPlantingCount(): number - getProvinceTeamCount(provinceCode: string): number - getCityTeamCount(cityCode: string): number -} - -export interface ITeamStatisticsRepository { - findByUserId(userId: string): Promise - findByAccountSequence(accountSequence: string): Promise -} - -export class AssessmentCalculatorService { - /** - * 计算月度考核 - */ - async calculateMonthlyAssessment( - authorization: AuthorizationRole, - assessmentMonth: Month, - teamStats: TeamStatistics, - repository: IMonthlyAssessmentRepository, - ): Promise { - // 1. 查找或创建本月考核 - let assessment = await repository.findByAuthorizationAndMonth( - authorization.authorizationId, - assessmentMonth, - ) - - if (!assessment) { - // 获取目标 - const monthIndex = authorization.currentMonthIndex || 1 - const target = LadderTargetRule.getTarget(authorization.roleType, monthIndex) - - assessment = MonthlyAssessment.create({ - authorizationId: authorization.authorizationId, - userId: authorization.userId, - roleType: authorization.roleType, - regionCode: authorization.regionCode, - assessmentMonth, - monthIndex, - monthlyTarget: target.monthlyTarget, - cumulativeTarget: target.cumulativeTarget, - }) - } - - // 2. 执行考核 - const localTeamCount = this.getLocalTeamCount( - teamStats, - authorization.roleType, - authorization.regionCode, - ) - - assessment.assess({ - cumulativeCompleted: teamStats.totalTeamPlantingCount, - localTeamCount, - totalTeamCount: teamStats.totalTeamPlantingCount, - requireLocalPercentage: authorization.requireLocalPercentage, - exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, - }) - - return assessment - } - - /** - * 批量评估并排名 - */ - async assessAndRankRegion( - roleType: RoleType, - regionCode: RegionCode, - assessmentMonth: Month, - authorizationRepository: IAuthorizationRoleRepository, - statsRepository: ITeamStatisticsRepository, - assessmentRepository: IMonthlyAssessmentRepository, - ): Promise { - // 1. 查找该区域的所有激活授权 - const authorizations = await authorizationRepository.findActiveByRoleTypeAndRegion( - roleType, - regionCode, - ) - - // 2. 计算所有考核 - const assessments: MonthlyAssessment[] = [] - - for (const auth of authorizations) { - const teamStats = await statsRepository.findByUserId(auth.userId.value) - if (!teamStats) continue - - const assessment = await this.calculateMonthlyAssessment( - auth, - assessmentMonth, - teamStats, - assessmentRepository, - ) - - assessments.push(assessment) - } - - // 3. 排名规则: - // - 按超越比例降序排列 - // - 比例相同时,按达标时间升序排列(先完成的排前面) - assessments.sort((a, b) => { - // 先按超越比例降序 - if (b.exceedRatio !== a.exceedRatio) { - return b.exceedRatio - a.exceedRatio - } - // 比例相同时按达标时间升序 - if (a.completedAt && b.completedAt) { - return a.completedAt.getTime() - b.completedAt.getTime() - } - // 有达标时间的排前面 - if (a.completedAt) return -1 - if (b.completedAt) return 1 - return 0 - }) - - // 4. 设置排名 - assessments.forEach((assessment, index) => { - assessment.setRanking(index + 1, index === 0) - }) - - return assessments - } - - private getLocalTeamCount( - teamStats: TeamStatistics, - roleType: RoleType, - regionCode: RegionCode, - ): number { - if ( - roleType === RoleType.AUTH_PROVINCE_COMPANY || - roleType === RoleType.PROVINCE_COMPANY - ) { - return teamStats.getProvinceTeamCount(regionCode.value) - } else if ( - roleType === RoleType.AUTH_CITY_COMPANY || - roleType === RoleType.CITY_COMPANY - ) { - return teamStats.getCityTeamCount(regionCode.value) - } - return 0 - } -} +import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' +import { LadderTargetRule } from '@/domain/entities' +import { Month, RegionCode } from '@/domain/value-objects' +import { RoleType } from '@/domain/enums' +import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/domain/repositories' + +export interface TeamStatistics { + userId: string + accountSequence: string + totalTeamPlantingCount: number + selfPlantingCount: number + /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ + get subordinateTeamPlantingCount(): number + getProvinceTeamCount(provinceCode: string): number + getCityTeamCount(cityCode: string): number +} + +export interface ITeamStatisticsRepository { + findByUserId(userId: string): Promise + findByAccountSequence(accountSequence: string): Promise + /** 获取用户的祖先链(推荐链),返回从直接推荐人到根节点的 accountSequence 列表 */ + getReferralChain(accountSequence: string): Promise +} + +export class AssessmentCalculatorService { + /** + * 计算月度考核 + */ + async calculateMonthlyAssessment( + authorization: AuthorizationRole, + assessmentMonth: Month, + teamStats: TeamStatistics, + repository: IMonthlyAssessmentRepository, + ): Promise { + // 1. 查找或创建本月考核 + let assessment = await repository.findByAuthorizationAndMonth( + authorization.authorizationId, + assessmentMonth, + ) + + if (!assessment) { + // 获取目标 + const monthIndex = authorization.currentMonthIndex || 1 + const target = LadderTargetRule.getTarget(authorization.roleType, monthIndex) + + assessment = MonthlyAssessment.create({ + authorizationId: authorization.authorizationId, + userId: authorization.userId, + roleType: authorization.roleType, + regionCode: authorization.regionCode, + assessmentMonth, + monthIndex, + monthlyTarget: target.monthlyTarget, + cumulativeTarget: target.cumulativeTarget, + }) + } + + // 2. 执行考核 + const localTeamCount = this.getLocalTeamCount( + teamStats, + authorization.roleType, + authorization.regionCode, + ) + + assessment.assess({ + cumulativeCompleted: teamStats.totalTeamPlantingCount, + localTeamCount, + totalTeamCount: teamStats.totalTeamPlantingCount, + requireLocalPercentage: authorization.requireLocalPercentage, + exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, + }) + + return assessment + } + + /** + * 批量评估并排名 + */ + async assessAndRankRegion( + roleType: RoleType, + regionCode: RegionCode, + assessmentMonth: Month, + authorizationRepository: IAuthorizationRoleRepository, + statsRepository: ITeamStatisticsRepository, + assessmentRepository: IMonthlyAssessmentRepository, + ): Promise { + // 1. 查找该区域的所有激活授权 + const authorizations = await authorizationRepository.findActiveByRoleTypeAndRegion( + roleType, + regionCode, + ) + + // 2. 计算所有考核 + const assessments: MonthlyAssessment[] = [] + + for (const auth of authorizations) { + const teamStats = await statsRepository.findByUserId(auth.userId.value) + if (!teamStats) continue + + const assessment = await this.calculateMonthlyAssessment( + auth, + assessmentMonth, + teamStats, + assessmentRepository, + ) + + assessments.push(assessment) + } + + // 3. 排名规则: + // - 按超越比例降序排列 + // - 比例相同时,按达标时间升序排列(先完成的排前面) + assessments.sort((a, b) => { + // 先按超越比例降序 + if (b.exceedRatio !== a.exceedRatio) { + return b.exceedRatio - a.exceedRatio + } + // 比例相同时按达标时间升序 + if (a.completedAt && b.completedAt) { + return a.completedAt.getTime() - b.completedAt.getTime() + } + // 有达标时间的排前面 + if (a.completedAt) return -1 + if (b.completedAt) return 1 + return 0 + }) + + // 4. 设置排名 + assessments.forEach((assessment, index) => { + assessment.setRanking(index + 1, index === 0) + }) + + return assessments + } + + private getLocalTeamCount( + teamStats: TeamStatistics, + roleType: RoleType, + regionCode: RegionCode, + ): number { + if ( + roleType === RoleType.AUTH_PROVINCE_COMPANY || + roleType === RoleType.PROVINCE_COMPANY + ) { + return teamStats.getProvinceTeamCount(regionCode.value) + } else if ( + roleType === RoleType.AUTH_CITY_COMPANY || + roleType === RoleType.CITY_COMPANY + ) { + return teamStats.getCityTeamCount(regionCode.value) + } + return 0 + } +} diff --git a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts index edc422de..88016f15 100644 --- a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts +++ b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts @@ -183,6 +183,9 @@ export class EventConsumerController { } } + // 4. 检查自动升级条件(省区域/市区域) + await this.checkAutoUpgrade(teamStats) + this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`) } catch (error) { this.logger.error(`[PLANTING] Error processing tree planted for user ${userId}:`, error) @@ -283,4 +286,204 @@ export class EventConsumerController { await this.eventPublisher.publishAll(assessment.domainEvents) assessment.clearDomainEvents() } + + /** + * 自动升级阈值常量 + */ + private static readonly PROVINCE_UPGRADE_THRESHOLD = 50000 // 5万棵 + private static readonly CITY_UPGRADE_THRESHOLD = 10000 // 1万棵 + + /** + * 检查并执行自动升级 + * 业务规则: + * - 省团队账号伞下第一个累计达到5万棵的账户自动获得该省区域授权 + * - 市团队账号伞下第一个累计达到1万棵的账户自动获得该市区域授权 + */ + private async checkAutoUpgrade(teamStats: TeamStatistics): Promise { + const userId = teamStats.userId + const accountSequence = teamStats.accountSequence + const totalTeamCount = teamStats.totalTeamPlantingCount + + this.logger.debug(`[AUTO-UPGRADE] Checking auto upgrade for user ${userId}, total team count: ${totalTeamCount}`) + + // 检查省区域升级(5万棵) + if (totalTeamCount >= EventConsumerController.PROVINCE_UPGRADE_THRESHOLD) { + await this.checkProvinceAutoUpgrade(teamStats) + } + + // 检查市区域升级(1万棵) + if (totalTeamCount >= EventConsumerController.CITY_UPGRADE_THRESHOLD) { + await this.checkCityAutoUpgrade(teamStats) + } + } + + /** + * 检查省区域自动升级 + * 条件:用户是某省团队的伞下成员,且累计达到5万棵,且该省尚无省区域授权 + */ + private async checkProvinceAutoUpgrade(teamStats: TeamStatistics): Promise { + const userId = UserId.create(teamStats.userId, teamStats.accountSequence) + const accountSequence = teamStats.accountSequence + + this.logger.debug(`[AUTO-UPGRADE] Checking province auto upgrade for ${accountSequence}`) + + // 1. 检查用户是否已有省区域授权 + const existingProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.PROVINCE_COMPANY, + ) + if (existingProvince && existingProvince.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has province company authorization`) + return + } + + // 2. 检查用户是否已有市区域授权(互斥) + const existingCity = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.CITY_COMPANY, + ) + if (existingCity && existingCity.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has city company authorization, cannot auto upgrade to province`) + return + } + + // 3. 获取用户的祖先链,查找上级省团队 + const ancestorSeqs = await this.statsRepository.getReferralChain(accountSequence) as unknown as string[] + if (ancestorSeqs.length === 0) { + this.logger.debug(`[AUTO-UPGRADE] No ancestors found for ${accountSequence}`) + return + } + + // 4. 查找祖先链中的省团队授权 + const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences(ancestorSeqs) + if (ancestorAuthProvinces.length === 0) { + this.logger.debug(`[AUTO-UPGRADE] No auth province found in ancestor chain for ${accountSequence}`) + return + } + + // 5. 取最近的上级省团队 + let nearestAuthProvince: AuthorizationRole | null = null + for (const ancestorSeq of ancestorSeqs) { + const found = ancestorAuthProvinces.find(auth => auth.userId.accountSequence === ancestorSeq) + if (found && found.benefitActive) { + nearestAuthProvince = found + break + } + } + + if (!nearestAuthProvince) { + this.logger.debug(`[AUTO-UPGRADE] No active auth province found in ancestor chain for ${accountSequence}`) + return + } + + const provinceCode = nearestAuthProvince.regionCode.value + const provinceName = nearestAuthProvince.regionName + + // 6. 检查该省是否已有省区域授权 + const existingProvinceRegion = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode) + if (existingProvinceRegion) { + this.logger.debug(`[AUTO-UPGRADE] Province ${provinceName} already has province company authorization`) + return + } + + // 7. 执行自动升级 + this.logger.log(`[AUTO-UPGRADE] Auto upgrading user ${accountSequence} to province company: ${provinceName}`) + + const authorization = AuthorizationRole.createAutoUpgradedProvinceCompany({ + userId, + provinceCode, + provinceName, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + this.logger.log(`[AUTO-UPGRADE] Successfully auto upgraded user ${accountSequence} to province company: ${provinceName}`) + } + + /** + * 检查市区域自动升级 + * 条件:用户是某市团队的伞下成员,且累计达到1万棵,且该市尚无市区域授权 + */ + private async checkCityAutoUpgrade(teamStats: TeamStatistics): Promise { + const userId = UserId.create(teamStats.userId, teamStats.accountSequence) + const accountSequence = teamStats.accountSequence + + this.logger.debug(`[AUTO-UPGRADE] Checking city auto upgrade for ${accountSequence}`) + + // 1. 检查用户是否已有市区域授权 + const existingCity = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.CITY_COMPANY, + ) + if (existingCity && existingCity.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has city company authorization`) + return + } + + // 2. 检查用户是否已有省区域授权(互斥) + const existingProvince = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.PROVINCE_COMPANY, + ) + if (existingProvince && existingProvince.status !== AuthorizationStatus.REVOKED) { + this.logger.debug(`[AUTO-UPGRADE] User ${accountSequence} already has province company authorization, cannot auto upgrade to city`) + return + } + + // 3. 获取用户的祖先链,查找上级市团队 + const ancestorSeqs = await this.statsRepository.getReferralChain(accountSequence) as unknown as string[] + if (ancestorSeqs.length === 0) { + this.logger.debug(`[AUTO-UPGRADE] No ancestors found for ${accountSequence}`) + return + } + + // 4. 查找祖先链中的市团队授权 + const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences(ancestorSeqs) + if (ancestorAuthCities.length === 0) { + this.logger.debug(`[AUTO-UPGRADE] No auth city found in ancestor chain for ${accountSequence}`) + return + } + + // 5. 取最近的上级市团队 + let nearestAuthCity: AuthorizationRole | null = null + for (const ancestorSeq of ancestorSeqs) { + const found = ancestorAuthCities.find(auth => auth.userId.accountSequence === ancestorSeq) + if (found && found.benefitActive) { + nearestAuthCity = found + break + } + } + + if (!nearestAuthCity) { + this.logger.debug(`[AUTO-UPGRADE] No active auth city found in ancestor chain for ${accountSequence}`) + return + } + + const cityCode = nearestAuthCity.regionCode.value + const cityName = nearestAuthCity.regionName + + // 6. 检查该市是否已有市区域授权 + const existingCityRegion = await this.authorizationRepository.findCityCompanyByRegion(cityCode) + if (existingCityRegion) { + this.logger.debug(`[AUTO-UPGRADE] City ${cityName} already has city company authorization`) + return + } + + // 7. 执行自动升级 + this.logger.log(`[AUTO-UPGRADE] Auto upgrading user ${accountSequence} to city company: ${cityName}`) + + const authorization = AuthorizationRole.createAutoUpgradedCityCompany({ + userId, + cityCode, + cityName, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + this.logger.log(`[AUTO-UPGRADE] Successfully auto upgraded user ${accountSequence} to city company: ${cityName}`) + } } diff --git a/backend/services/authorization-service/test/domain-services.integration-spec.ts b/backend/services/authorization-service/test/domain-services.integration-spec.ts index ca3b8f45..a9b9a2b3 100644 --- a/backend/services/authorization-service/test/domain-services.integration-spec.ts +++ b/backend/services/authorization-service/test/domain-services.integration-spec.ts @@ -1,436 +1,437 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service' -import { AssessmentCalculatorService, TeamStatistics, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service' -import { IAuthorizationRoleRepository } from '@/domain/repositories/authorization-role.repository' -import { IMonthlyAssessmentRepository } from '@/domain/repositories/monthly-assessment.repository' -import { AUTHORIZATION_ROLE_REPOSITORY } from '@/infrastructure/persistence/repositories/authorization-role.repository.impl' -import { MONTHLY_ASSESSMENT_REPOSITORY } from '@/infrastructure/persistence/repositories/monthly-assessment.repository.impl' -import { UserId, RegionCode, Month, AuthorizationId } from '@/domain/value-objects' -import { RoleType, AuthorizationStatus } from '@/domain/enums' -import { AuthorizationRole } from '@/domain/aggregates' -import { LadderTargetRule } from '@/domain/entities' - -describe('Domain Services Integration Tests', () => { - let module: TestingModule - let validatorService: AuthorizationValidatorService - let calculatorService: AssessmentCalculatorService - - // Mock repositories - const mockAuthorizationRoleRepository: jest.Mocked = { - save: jest.fn(), - findById: jest.fn(), - findByUserIdAndRoleType: jest.fn(), - findByUserIdRoleTypeAndRegion: jest.fn(), - findByUserId: jest.fn(), - findActiveByRoleTypeAndRegion: jest.fn(), - findAllActive: jest.fn(), - findPendingByUserId: jest.fn(), - findByStatus: jest.fn(), - delete: jest.fn(), - findByAccountSequenceAndRoleType: jest.fn(), - findByAccountSequence: jest.fn(), - findActiveCommunityByAccountSequences: jest.fn(), - findActiveProvinceByAccountSequencesAndRegion: jest.fn(), - findActiveCityByAccountSequencesAndRegion: jest.fn(), - findCommunityByAccountSequences: jest.fn(), - findAuthProvinceByAccountSequencesAndRegion: jest.fn(), - findAuthCityByAccountSequencesAndRegion: jest.fn(), - findAuthProvinceByAccountSequences: jest.fn(), - findAuthCityByAccountSequences: jest.fn(), - findProvinceCompanyByRegion: jest.fn(), - findCityCompanyByRegion: jest.fn(), - findExpiredActiveByRoleType: jest.fn(), - findCommunityByName: jest.fn(), - findAllByUserIdIncludeDeleted: jest.fn(), - findAllByAccountSequenceIncludeDeleted: jest.fn(), - findByIdIncludeDeleted: jest.fn(), - } - - const mockMonthlyAssessmentRepository: jest.Mocked = { - save: jest.fn(), - saveAll: jest.fn(), - findById: jest.fn(), - findByAuthorizationAndMonth: jest.fn(), - findByUserAndMonth: jest.fn(), - findFirstByAuthorization: jest.fn(), - findByMonthAndRegion: jest.fn(), - findRankingsByMonthAndRegion: jest.fn(), - findByAuthorization: jest.fn(), - delete: jest.fn(), - } - - // Mock referral repository - const mockReferralRepository: jest.Mocked = { - findByUserId: jest.fn(), - getAllAncestors: jest.fn(), - getAllDescendants: jest.fn(), - } - - // Mock team statistics repository - const mockTeamStatisticsRepository: jest.Mocked = { - findByUserId: jest.fn(), - findByAccountSequence: jest.fn(), - } - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - AuthorizationValidatorService, - AssessmentCalculatorService, - { - provide: AUTHORIZATION_ROLE_REPOSITORY, - useValue: mockAuthorizationRoleRepository, - }, - { - provide: MONTHLY_ASSESSMENT_REPOSITORY, - useValue: mockMonthlyAssessmentRepository, - }, - { - provide: 'REFERRAL_REPOSITORY', - useValue: mockReferralRepository, - }, - { - provide: 'TEAM_STATISTICS_REPOSITORY', - useValue: mockTeamStatisticsRepository, - }, - ], - }).compile() - - validatorService = module.get(AuthorizationValidatorService) - calculatorService = module.get(AssessmentCalculatorService) - }) - - afterAll(async () => { - await module.close() - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('AuthorizationValidatorService', () => { - describe('validateAuthorizationRequest', () => { - it('should return success when no conflicts in referral chain', async () => { - const userId = UserId.create('user-123', 'D2412190123') - const roleType = RoleType.AUTH_PROVINCE_COMPANY - const regionCode = RegionCode.create('430000') - - mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) - mockReferralRepository.findByUserId.mockResolvedValue(null) - - const result = await validatorService.validateAuthorizationRequest( - userId, - roleType, - regionCode, - mockReferralRepository, - mockAuthorizationRoleRepository, - ) - - expect(result.isValid).toBe(true) - }) - - it('should return failure when user already has province authorization', async () => { - const userId = UserId.create('user-123', 'D2412190123') - const roleType = RoleType.AUTH_PROVINCE_COMPANY - const regionCode = RegionCode.create('430000') - - const existingAuth = AuthorizationRole.createAuthProvinceCompany({ - userId, - provinceCode: '440000', - provinceName: '广东省', - }) - mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth) - - const result = await validatorService.validateAuthorizationRequest( - userId, - roleType, - regionCode, - mockReferralRepository, - mockAuthorizationRoleRepository, - ) - - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('只能申请一个省代或市代授权') - }) - - it('should return failure when ancestor has same region authorization', async () => { - const userId = UserId.create('user-123', 'D2412190123') - const roleType = RoleType.AUTH_PROVINCE_COMPANY - const regionCode = RegionCode.create('430000') - - const ancestorUserId = UserId.create('ancestor-user', 'D2412190999') - - mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) - mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' }) - mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId]) - mockReferralRepository.getAllDescendants.mockResolvedValue([]) - - const existingAuth = AuthorizationRole.createAuthProvinceCompany({ - userId: ancestorUserId, - provinceCode: '430000', - provinceName: '湖南省', - }) - mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth) - - const result = await validatorService.validateAuthorizationRequest( - userId, - roleType, - regionCode, - mockReferralRepository, - mockAuthorizationRoleRepository, - ) - - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('本团队已有人申请') - }) - }) - }) - - describe('AssessmentCalculatorService', () => { - describe('assessAndRankRegion', () => { - it('should calculate assessments and rank by exceed ratio', async () => { - const roleType = RoleType.AUTH_PROVINCE_COMPANY - const regionCode = RegionCode.create('430000') - const assessmentMonth = Month.current() - - // Mock two authorizations - const auth1 = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1', 'D2412190001'), - provinceCode: '430000', - provinceName: '湖南省', - }) - auth1.authorize(UserId.create('admin', 'D2412190100')) - - const auth2 = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-2', 'D2412190002'), - provinceCode: '430000', - provinceName: '湖南省', - }) - auth2.authorize(UserId.create('admin', 'D2412190100')) - - mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2]) - mockMonthlyAssessmentRepository.findByAuthorizationAndMonth.mockResolvedValue(null) - - // User 1 has better stats - mockTeamStatisticsRepository.findByUserId - .mockResolvedValueOnce({ - userId: 'user-1', - accountSequence: 'D2412190001', - totalTeamPlantingCount: 200, - selfPlantingCount: 10, - get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, - getProvinceTeamCount: () => 70, - getCityTeamCount: () => 0, - }) - .mockResolvedValueOnce({ - userId: 'user-2', - accountSequence: 'D2412190002', - totalTeamPlantingCount: 100, - selfPlantingCount: 5, - get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, - getProvinceTeamCount: () => 35, - getCityTeamCount: () => 0, - }) - - const assessments = await calculatorService.assessAndRankRegion( - roleType, - regionCode, - assessmentMonth, - mockAuthorizationRoleRepository, - mockTeamStatisticsRepository, - mockMonthlyAssessmentRepository, - ) - - expect(assessments.length).toBe(2) - // User 1 should rank first (higher exceed ratio) - expect(assessments[0].userId.value).toBe('user-1') - expect(assessments[0].rankingInRegion).toBe(1) - expect(assessments[0].isFirstPlace).toBe(true) - }) - }) - }) -}) - -describe('LadderTargetRule Entity Integration Tests', () => { - describe('getTarget', () => { - it('should return correct target for province month 1', () => { - const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 1) - - expect(target.monthlyTarget).toBe(150) - expect(target.cumulativeTarget).toBe(150) - }) - - it('should return correct target for province month 5', () => { - const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 5) - - expect(target.monthlyTarget).toBe(2400) - expect(target.cumulativeTarget).toBe(4650) - }) - - it('should return month 9 target for months beyond 9', () => { - const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 12) - - expect(target.monthlyTarget).toBe(11750) - expect(target.cumulativeTarget).toBe(50000) - }) - - it('should return correct target for city month 1', () => { - const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 1) - - expect(target.monthlyTarget).toBe(30) - expect(target.cumulativeTarget).toBe(30) - }) - - it('should return correct target for city month 9', () => { - const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 9) - - expect(target.monthlyTarget).toBe(2350) - expect(target.cumulativeTarget).toBe(10000) - }) - - it('should return fixed target for community', () => { - const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1) - - expect(target.monthlyTarget).toBe(10) - expect(target.cumulativeTarget).toBe(10) - }) - }) - - describe('getFinalTarget', () => { - it('should return 50000 for province', () => { - expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_PROVINCE_COMPANY)).toBe(50000) - }) - - it('should return 10000 for city', () => { - expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_CITY_COMPANY)).toBe(10000) - }) - - it('should return 10 for community', () => { - expect(LadderTargetRule.getFinalTarget(RoleType.COMMUNITY)).toBe(10) - }) - }) - - describe('getAllTargets', () => { - it('should return 9 targets for province', () => { - const targets = LadderTargetRule.getAllTargets(RoleType.AUTH_PROVINCE_COMPANY) - expect(targets.length).toBe(9) - }) - - it('should return 9 targets for city', () => { - const targets = LadderTargetRule.getAllTargets(RoleType.AUTH_CITY_COMPANY) - expect(targets.length).toBe(9) - }) - - it('should return 1 target for community', () => { - const targets = LadderTargetRule.getAllTargets(RoleType.COMMUNITY) - expect(targets.length).toBe(1) - }) - }) -}) - -describe('Month Value Object Integration Tests', () => { - describe('create and format', () => { - it('should create month from string', () => { - const month = Month.create('2024-03') - expect(month.value).toBe('2024-03') - }) - - it('should create month from date', () => { - const date = new Date('2024-06-15') - const month = Month.fromDate(date) - expect(month.value).toBe('2024-06') - }) - - it('should create current month', () => { - const month = Month.current() - const now = new Date() - const expectedMonth = String(now.getMonth() + 1).padStart(2, '0') - const expectedYear = now.getFullYear() - expect(month.value).toBe(`${expectedYear}-${expectedMonth}`) - }) - }) - - describe('navigation', () => { - it('should get next month correctly', () => { - const month = Month.create('2024-03') - const next = month.next() - expect(next.value).toBe('2024-04') - }) - - it('should handle year boundary for next', () => { - const month = Month.create('2024-12') - const next = month.next() - expect(next.value).toBe('2025-01') - }) - - it('should get previous month correctly', () => { - const month = Month.create('2024-03') - const prev = month.previous() - expect(prev.value).toBe('2024-02') - }) - - it('should handle year boundary for previous', () => { - const month = Month.create('2024-01') - const prev = month.previous() - expect(prev.value).toBe('2023-12') - }) - }) - - describe('comparison', () => { - it('should compare months correctly', () => { - const month1 = Month.create('2024-03') - const month2 = Month.create('2024-06') - - expect(month1.isBefore(month2)).toBe(true) - expect(month2.isAfter(month1)).toBe(true) - expect(month1.equals(month1)).toBe(true) - }) - - it('should compare across years', () => { - const month1 = Month.create('2023-12') - const month2 = Month.create('2024-01') - - expect(month1.isBefore(month2)).toBe(true) - expect(month2.isAfter(month1)).toBe(true) - }) - }) - - describe('extraction', () => { - it('should extract year and month', () => { - const month = Month.create('2024-03') - - expect(month.getYear()).toBe(2024) - expect(month.getMonth()).toBe(3) - }) - }) -}) - -describe('RegionCode Value Object Integration Tests', () => { - describe('create', () => { - it('should create valid region code', () => { - const regionCode = RegionCode.create('430000') - expect(regionCode.value).toBe('430000') - }) - - it('should create city code', () => { - const regionCode = RegionCode.create('430100') - expect(regionCode.value).toBe('430100') - }) - }) - - describe('equality', () => { - it('should compare equal region codes', () => { - const code1 = RegionCode.create('430000') - const code2 = RegionCode.create('430000') - - expect(code1.equals(code2)).toBe(true) - }) - - it('should compare different region codes', () => { - const code1 = RegionCode.create('430000') - const code2 = RegionCode.create('440000') - - expect(code1.equals(code2)).toBe(false) - }) - }) -}) +import { Test, TestingModule } from '@nestjs/testing' +import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service' +import { AssessmentCalculatorService, TeamStatistics, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service' +import { IAuthorizationRoleRepository } from '@/domain/repositories/authorization-role.repository' +import { IMonthlyAssessmentRepository } from '@/domain/repositories/monthly-assessment.repository' +import { AUTHORIZATION_ROLE_REPOSITORY } from '@/infrastructure/persistence/repositories/authorization-role.repository.impl' +import { MONTHLY_ASSESSMENT_REPOSITORY } from '@/infrastructure/persistence/repositories/monthly-assessment.repository.impl' +import { UserId, RegionCode, Month, AuthorizationId } from '@/domain/value-objects' +import { RoleType, AuthorizationStatus } from '@/domain/enums' +import { AuthorizationRole } from '@/domain/aggregates' +import { LadderTargetRule } from '@/domain/entities' + +describe('Domain Services Integration Tests', () => { + let module: TestingModule + let validatorService: AuthorizationValidatorService + let calculatorService: AssessmentCalculatorService + + // Mock repositories + const mockAuthorizationRoleRepository: jest.Mocked = { + save: jest.fn(), + findById: jest.fn(), + findByUserIdAndRoleType: jest.fn(), + findByUserIdRoleTypeAndRegion: jest.fn(), + findByUserId: jest.fn(), + findActiveByRoleTypeAndRegion: jest.fn(), + findAllActive: jest.fn(), + findPendingByUserId: jest.fn(), + findByStatus: jest.fn(), + delete: jest.fn(), + findByAccountSequenceAndRoleType: jest.fn(), + findByAccountSequence: jest.fn(), + findActiveCommunityByAccountSequences: jest.fn(), + findActiveProvinceByAccountSequencesAndRegion: jest.fn(), + findActiveCityByAccountSequencesAndRegion: jest.fn(), + findCommunityByAccountSequences: jest.fn(), + findAuthProvinceByAccountSequencesAndRegion: jest.fn(), + findAuthCityByAccountSequencesAndRegion: jest.fn(), + findAuthProvinceByAccountSequences: jest.fn(), + findAuthCityByAccountSequences: jest.fn(), + findProvinceCompanyByRegion: jest.fn(), + findCityCompanyByRegion: jest.fn(), + findExpiredActiveByRoleType: jest.fn(), + findCommunityByName: jest.fn(), + findAllByUserIdIncludeDeleted: jest.fn(), + findAllByAccountSequenceIncludeDeleted: jest.fn(), + findByIdIncludeDeleted: jest.fn(), + } + + const mockMonthlyAssessmentRepository: jest.Mocked = { + save: jest.fn(), + saveAll: jest.fn(), + findById: jest.fn(), + findByAuthorizationAndMonth: jest.fn(), + findByUserAndMonth: jest.fn(), + findFirstByAuthorization: jest.fn(), + findByMonthAndRegion: jest.fn(), + findRankingsByMonthAndRegion: jest.fn(), + findByAuthorization: jest.fn(), + delete: jest.fn(), + } + + // Mock referral repository + const mockReferralRepository: jest.Mocked = { + findByUserId: jest.fn(), + getAllAncestors: jest.fn(), + getAllDescendants: jest.fn(), + } + + // Mock team statistics repository + const mockTeamStatisticsRepository: jest.Mocked = { + findByUserId: jest.fn(), + findByAccountSequence: jest.fn(), + getReferralChain: jest.fn(), + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + AuthorizationValidatorService, + AssessmentCalculatorService, + { + provide: AUTHORIZATION_ROLE_REPOSITORY, + useValue: mockAuthorizationRoleRepository, + }, + { + provide: MONTHLY_ASSESSMENT_REPOSITORY, + useValue: mockMonthlyAssessmentRepository, + }, + { + provide: 'REFERRAL_REPOSITORY', + useValue: mockReferralRepository, + }, + { + provide: 'TEAM_STATISTICS_REPOSITORY', + useValue: mockTeamStatisticsRepository, + }, + ], + }).compile() + + validatorService = module.get(AuthorizationValidatorService) + calculatorService = module.get(AssessmentCalculatorService) + }) + + afterAll(async () => { + await module.close() + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('AuthorizationValidatorService', () => { + describe('validateAuthorizationRequest', () => { + it('should return success when no conflicts in referral chain', async () => { + const userId = UserId.create('user-123', 'D2412190123') + const roleType = RoleType.AUTH_PROVINCE_COMPANY + const regionCode = RegionCode.create('430000') + + mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) + mockReferralRepository.findByUserId.mockResolvedValue(null) + + const result = await validatorService.validateAuthorizationRequest( + userId, + roleType, + regionCode, + mockReferralRepository, + mockAuthorizationRoleRepository, + ) + + expect(result.isValid).toBe(true) + }) + + it('should return failure when user already has province authorization', async () => { + const userId = UserId.create('user-123', 'D2412190123') + const roleType = RoleType.AUTH_PROVINCE_COMPANY + const regionCode = RegionCode.create('430000') + + const existingAuth = AuthorizationRole.createAuthProvinceCompany({ + userId, + provinceCode: '440000', + provinceName: '广东省', + }) + mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth) + + const result = await validatorService.validateAuthorizationRequest( + userId, + roleType, + regionCode, + mockReferralRepository, + mockAuthorizationRoleRepository, + ) + + expect(result.isValid).toBe(false) + expect(result.errorMessage).toContain('只能申请一个省代或市代授权') + }) + + it('should return failure when ancestor has same region authorization', async () => { + const userId = UserId.create('user-123', 'D2412190123') + const roleType = RoleType.AUTH_PROVINCE_COMPANY + const regionCode = RegionCode.create('430000') + + const ancestorUserId = UserId.create('ancestor-user', 'D2412190999') + + mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) + mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' }) + mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId]) + mockReferralRepository.getAllDescendants.mockResolvedValue([]) + + const existingAuth = AuthorizationRole.createAuthProvinceCompany({ + userId: ancestorUserId, + provinceCode: '430000', + provinceName: '湖南省', + }) + mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth) + + const result = await validatorService.validateAuthorizationRequest( + userId, + roleType, + regionCode, + mockReferralRepository, + mockAuthorizationRoleRepository, + ) + + expect(result.isValid).toBe(false) + expect(result.errorMessage).toContain('本团队已有人申请') + }) + }) + }) + + describe('AssessmentCalculatorService', () => { + describe('assessAndRankRegion', () => { + it('should calculate assessments and rank by exceed ratio', async () => { + const roleType = RoleType.AUTH_PROVINCE_COMPANY + const regionCode = RegionCode.create('430000') + const assessmentMonth = Month.current() + + // Mock two authorizations + const auth1 = AuthorizationRole.createAuthProvinceCompany({ + userId: UserId.create('user-1', 'D2412190001'), + provinceCode: '430000', + provinceName: '湖南省', + }) + auth1.authorize(UserId.create('admin', 'D2412190100')) + + const auth2 = AuthorizationRole.createAuthProvinceCompany({ + userId: UserId.create('user-2', 'D2412190002'), + provinceCode: '430000', + provinceName: '湖南省', + }) + auth2.authorize(UserId.create('admin', 'D2412190100')) + + mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2]) + mockMonthlyAssessmentRepository.findByAuthorizationAndMonth.mockResolvedValue(null) + + // User 1 has better stats + mockTeamStatisticsRepository.findByUserId + .mockResolvedValueOnce({ + userId: 'user-1', + accountSequence: 'D2412190001', + totalTeamPlantingCount: 200, + selfPlantingCount: 10, + get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, + getProvinceTeamCount: () => 70, + getCityTeamCount: () => 0, + }) + .mockResolvedValueOnce({ + userId: 'user-2', + accountSequence: 'D2412190002', + totalTeamPlantingCount: 100, + selfPlantingCount: 5, + get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, + getProvinceTeamCount: () => 35, + getCityTeamCount: () => 0, + }) + + const assessments = await calculatorService.assessAndRankRegion( + roleType, + regionCode, + assessmentMonth, + mockAuthorizationRoleRepository, + mockTeamStatisticsRepository, + mockMonthlyAssessmentRepository, + ) + + expect(assessments.length).toBe(2) + // User 1 should rank first (higher exceed ratio) + expect(assessments[0].userId.value).toBe('user-1') + expect(assessments[0].rankingInRegion).toBe(1) + expect(assessments[0].isFirstPlace).toBe(true) + }) + }) + }) +}) + +describe('LadderTargetRule Entity Integration Tests', () => { + describe('getTarget', () => { + it('should return correct target for province month 1', () => { + const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 1) + + expect(target.monthlyTarget).toBe(150) + expect(target.cumulativeTarget).toBe(150) + }) + + it('should return correct target for province month 5', () => { + const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 5) + + expect(target.monthlyTarget).toBe(2400) + expect(target.cumulativeTarget).toBe(4650) + }) + + it('should return month 9 target for months beyond 9', () => { + const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 12) + + expect(target.monthlyTarget).toBe(11750) + expect(target.cumulativeTarget).toBe(50000) + }) + + it('should return correct target for city month 1', () => { + const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 1) + + expect(target.monthlyTarget).toBe(30) + expect(target.cumulativeTarget).toBe(30) + }) + + it('should return correct target for city month 9', () => { + const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 9) + + expect(target.monthlyTarget).toBe(2350) + expect(target.cumulativeTarget).toBe(10000) + }) + + it('should return fixed target for community', () => { + const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1) + + expect(target.monthlyTarget).toBe(10) + expect(target.cumulativeTarget).toBe(10) + }) + }) + + describe('getFinalTarget', () => { + it('should return 50000 for province', () => { + expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_PROVINCE_COMPANY)).toBe(50000) + }) + + it('should return 10000 for city', () => { + expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_CITY_COMPANY)).toBe(10000) + }) + + it('should return 10 for community', () => { + expect(LadderTargetRule.getFinalTarget(RoleType.COMMUNITY)).toBe(10) + }) + }) + + describe('getAllTargets', () => { + it('should return 9 targets for province', () => { + const targets = LadderTargetRule.getAllTargets(RoleType.AUTH_PROVINCE_COMPANY) + expect(targets.length).toBe(9) + }) + + it('should return 9 targets for city', () => { + const targets = LadderTargetRule.getAllTargets(RoleType.AUTH_CITY_COMPANY) + expect(targets.length).toBe(9) + }) + + it('should return 1 target for community', () => { + const targets = LadderTargetRule.getAllTargets(RoleType.COMMUNITY) + expect(targets.length).toBe(1) + }) + }) +}) + +describe('Month Value Object Integration Tests', () => { + describe('create and format', () => { + it('should create month from string', () => { + const month = Month.create('2024-03') + expect(month.value).toBe('2024-03') + }) + + it('should create month from date', () => { + const date = new Date('2024-06-15') + const month = Month.fromDate(date) + expect(month.value).toBe('2024-06') + }) + + it('should create current month', () => { + const month = Month.current() + const now = new Date() + const expectedMonth = String(now.getMonth() + 1).padStart(2, '0') + const expectedYear = now.getFullYear() + expect(month.value).toBe(`${expectedYear}-${expectedMonth}`) + }) + }) + + describe('navigation', () => { + it('should get next month correctly', () => { + const month = Month.create('2024-03') + const next = month.next() + expect(next.value).toBe('2024-04') + }) + + it('should handle year boundary for next', () => { + const month = Month.create('2024-12') + const next = month.next() + expect(next.value).toBe('2025-01') + }) + + it('should get previous month correctly', () => { + const month = Month.create('2024-03') + const prev = month.previous() + expect(prev.value).toBe('2024-02') + }) + + it('should handle year boundary for previous', () => { + const month = Month.create('2024-01') + const prev = month.previous() + expect(prev.value).toBe('2023-12') + }) + }) + + describe('comparison', () => { + it('should compare months correctly', () => { + const month1 = Month.create('2024-03') + const month2 = Month.create('2024-06') + + expect(month1.isBefore(month2)).toBe(true) + expect(month2.isAfter(month1)).toBe(true) + expect(month1.equals(month1)).toBe(true) + }) + + it('should compare across years', () => { + const month1 = Month.create('2023-12') + const month2 = Month.create('2024-01') + + expect(month1.isBefore(month2)).toBe(true) + expect(month2.isAfter(month1)).toBe(true) + }) + }) + + describe('extraction', () => { + it('should extract year and month', () => { + const month = Month.create('2024-03') + + expect(month.getYear()).toBe(2024) + expect(month.getMonth()).toBe(3) + }) + }) +}) + +describe('RegionCode Value Object Integration Tests', () => { + describe('create', () => { + it('should create valid region code', () => { + const regionCode = RegionCode.create('430000') + expect(regionCode.value).toBe('430000') + }) + + it('should create city code', () => { + const regionCode = RegionCode.create('430100') + expect(regionCode.value).toBe('430100') + }) + }) + + describe('equality', () => { + it('should compare equal region codes', () => { + const code1 = RegionCode.create('430000') + const code2 = RegionCode.create('430000') + + expect(code1.equals(code2)).toBe(true) + }) + + it('should compare different region codes', () => { + const code1 = RegionCode.create('430000') + const code2 = RegionCode.create('440000') + + expect(code1.equals(code2)).toBe(false) + }) + }) +})