feat(authorization): 实现省区域/市区域自动升级机制

- 添加 createAutoUpgradedProvinceCompany/CityCompany 工厂方法
- 在 handleTreePlanted 中检查自动升级条件
- 省区域:团队累计5万棵时自动升级
- 市区域:团队累计1万棵时自动升级
- 扩展 ITeamStatisticsRepository 接口添加 getReferralChain 方法

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-22 17:24:44 -08:00
parent 0506a3547c
commit 7a264a0158
5 changed files with 898 additions and 592 deletions

View File

@ -675,6 +675,106 @@ export class AuthorizationRole extends AggregateRoot {
return auth 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授权省团队 // 工厂方法 - Admin授权省团队
static createAuthProvinceCompanyByAdmin(params: { static createAuthProvinceCompanyByAdmin(params: {
userId: UserId userId: UserId

View File

@ -96,7 +96,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent {
userId: string userId: string
provinceCode: string provinceCode: string
provinceName: string provinceName: string
authorizedBy: string authorizedBy: string | null // null 表示自动升级
} }
constructor(data: { constructor(data: {
@ -104,7 +104,7 @@ export class ProvinceCompanyAuthorizedEvent extends DomainEvent {
userId: string userId: string
provinceCode: string provinceCode: string
provinceName: string provinceName: string
authorizedBy: string authorizedBy: string | null
}) { }) {
super() super()
this.aggregateId = data.authorizationId this.aggregateId = data.authorizationId
@ -121,7 +121,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent {
userId: string userId: string
cityCode: string cityCode: string
cityName: string cityName: string
authorizedBy: string authorizedBy: string | null // null 表示自动升级
} }
constructor(data: { constructor(data: {
@ -129,7 +129,7 @@ export class CityCompanyAuthorizedEvent extends DomainEvent {
userId: string userId: string
cityCode: string cityCode: string
cityName: string cityName: string
authorizedBy: string authorizedBy: string | null
}) { }) {
super() super()
this.aggregateId = data.authorizationId this.aggregateId = data.authorizationId

View File

@ -1,152 +1,154 @@
import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates'
import { LadderTargetRule } from '@/domain/entities' import { LadderTargetRule } from '@/domain/entities'
import { Month, RegionCode } from '@/domain/value-objects' import { Month, RegionCode } from '@/domain/value-objects'
import { RoleType } from '@/domain/enums' import { RoleType } from '@/domain/enums'
import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/domain/repositories' import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/domain/repositories'
export interface TeamStatistics { export interface TeamStatistics {
userId: string userId: string
accountSequence: string accountSequence: string
totalTeamPlantingCount: number totalTeamPlantingCount: number
selfPlantingCount: number selfPlantingCount: number
/** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */
get subordinateTeamPlantingCount(): number get subordinateTeamPlantingCount(): number
getProvinceTeamCount(provinceCode: string): number getProvinceTeamCount(provinceCode: string): number
getCityTeamCount(cityCode: string): number getCityTeamCount(cityCode: string): number
} }
export interface ITeamStatisticsRepository { export interface ITeamStatisticsRepository {
findByUserId(userId: string): Promise<TeamStatistics | null> findByUserId(userId: string): Promise<TeamStatistics | null>
findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null> findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
} /** 获取用户的祖先链(推荐链),返回从直接推荐人到根节点的 accountSequence 列表 */
getReferralChain(accountSequence: string): Promise<string[]>
export class AssessmentCalculatorService { }
/**
* export class AssessmentCalculatorService {
*/ /**
async calculateMonthlyAssessment( *
authorization: AuthorizationRole, */
assessmentMonth: Month, async calculateMonthlyAssessment(
teamStats: TeamStatistics, authorization: AuthorizationRole,
repository: IMonthlyAssessmentRepository, assessmentMonth: Month,
): Promise<MonthlyAssessment> { teamStats: TeamStatistics,
// 1. 查找或创建本月考核 repository: IMonthlyAssessmentRepository,
let assessment = await repository.findByAuthorizationAndMonth( ): Promise<MonthlyAssessment> {
authorization.authorizationId, // 1. 查找或创建本月考核
assessmentMonth, let assessment = await repository.findByAuthorizationAndMonth(
) authorization.authorizationId,
assessmentMonth,
if (!assessment) { )
// 获取目标
const monthIndex = authorization.currentMonthIndex || 1 if (!assessment) {
const target = LadderTargetRule.getTarget(authorization.roleType, monthIndex) // 获取目标
const monthIndex = authorization.currentMonthIndex || 1
assessment = MonthlyAssessment.create({ const target = LadderTargetRule.getTarget(authorization.roleType, monthIndex)
authorizationId: authorization.authorizationId,
userId: authorization.userId, assessment = MonthlyAssessment.create({
roleType: authorization.roleType, authorizationId: authorization.authorizationId,
regionCode: authorization.regionCode, userId: authorization.userId,
assessmentMonth, roleType: authorization.roleType,
monthIndex, regionCode: authorization.regionCode,
monthlyTarget: target.monthlyTarget, assessmentMonth,
cumulativeTarget: target.cumulativeTarget, monthIndex,
}) monthlyTarget: target.monthlyTarget,
} cumulativeTarget: target.cumulativeTarget,
})
// 2. 执行考核 }
const localTeamCount = this.getLocalTeamCount(
teamStats, // 2. 执行考核
authorization.roleType, const localTeamCount = this.getLocalTeamCount(
authorization.regionCode, teamStats,
) authorization.roleType,
authorization.regionCode,
assessment.assess({ )
cumulativeCompleted: teamStats.totalTeamPlantingCount,
localTeamCount, assessment.assess({
totalTeamCount: teamStats.totalTeamPlantingCount, cumulativeCompleted: teamStats.totalTeamPlantingCount,
requireLocalPercentage: authorization.requireLocalPercentage, localTeamCount,
exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, totalTeamCount: teamStats.totalTeamPlantingCount,
}) requireLocalPercentage: authorization.requireLocalPercentage,
exemptFromPercentageCheck: authorization.exemptFromPercentageCheck,
return assessment })
}
return assessment
/** }
*
*/ /**
async assessAndRankRegion( *
roleType: RoleType, */
regionCode: RegionCode, async assessAndRankRegion(
assessmentMonth: Month, roleType: RoleType,
authorizationRepository: IAuthorizationRoleRepository, regionCode: RegionCode,
statsRepository: ITeamStatisticsRepository, assessmentMonth: Month,
assessmentRepository: IMonthlyAssessmentRepository, authorizationRepository: IAuthorizationRoleRepository,
): Promise<MonthlyAssessment[]> { statsRepository: ITeamStatisticsRepository,
// 1. 查找该区域的所有激活授权 assessmentRepository: IMonthlyAssessmentRepository,
const authorizations = await authorizationRepository.findActiveByRoleTypeAndRegion( ): Promise<MonthlyAssessment[]> {
roleType, // 1. 查找该区域的所有激活授权
regionCode, const authorizations = await authorizationRepository.findActiveByRoleTypeAndRegion(
) roleType,
regionCode,
// 2. 计算所有考核 )
const assessments: MonthlyAssessment[] = []
// 2. 计算所有考核
for (const auth of authorizations) { const assessments: MonthlyAssessment[] = []
const teamStats = await statsRepository.findByUserId(auth.userId.value)
if (!teamStats) continue for (const auth of authorizations) {
const teamStats = await statsRepository.findByUserId(auth.userId.value)
const assessment = await this.calculateMonthlyAssessment( if (!teamStats) continue
auth,
assessmentMonth, const assessment = await this.calculateMonthlyAssessment(
teamStats, auth,
assessmentRepository, assessmentMonth,
) teamStats,
assessmentRepository,
assessments.push(assessment) )
}
assessments.push(assessment)
// 3. 排名规则: }
// - 按超越比例降序排列
// - 比例相同时,按达标时间升序排列(先完成的排前面) // 3. 排名规则:
assessments.sort((a, b) => { // - 按超越比例降序排列
// 先按超越比例降序 // - 比例相同时,按达标时间升序排列(先完成的排前面)
if (b.exceedRatio !== a.exceedRatio) { assessments.sort((a, b) => {
return b.exceedRatio - a.exceedRatio // 先按超越比例降序
} 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 && b.completedAt) {
// 有达标时间的排前面 return a.completedAt.getTime() - b.completedAt.getTime()
if (a.completedAt) return -1 }
if (b.completedAt) return 1 // 有达标时间的排前面
return 0 if (a.completedAt) return -1
}) if (b.completedAt) return 1
return 0
// 4. 设置排名 })
assessments.forEach((assessment, index) => {
assessment.setRanking(index + 1, index === 0) // 4. 设置排名
}) assessments.forEach((assessment, index) => {
assessment.setRanking(index + 1, index === 0)
return assessments })
}
return assessments
private getLocalTeamCount( }
teamStats: TeamStatistics,
roleType: RoleType, private getLocalTeamCount(
regionCode: RegionCode, teamStats: TeamStatistics,
): number { roleType: RoleType,
if ( regionCode: RegionCode,
roleType === RoleType.AUTH_PROVINCE_COMPANY || ): number {
roleType === RoleType.PROVINCE_COMPANY if (
) { roleType === RoleType.AUTH_PROVINCE_COMPANY ||
return teamStats.getProvinceTeamCount(regionCode.value) roleType === RoleType.PROVINCE_COMPANY
} else if ( ) {
roleType === RoleType.AUTH_CITY_COMPANY || return teamStats.getProvinceTeamCount(regionCode.value)
roleType === RoleType.CITY_COMPANY } else if (
) { roleType === RoleType.AUTH_CITY_COMPANY ||
return teamStats.getCityTeamCount(regionCode.value) roleType === RoleType.CITY_COMPANY
} ) {
return 0 return teamStats.getCityTeamCount(regionCode.value)
} }
} return 0
}
}

View File

@ -183,6 +183,9 @@ export class EventConsumerController {
} }
} }
// 4. 检查自动升级条件(省区域/市区域)
await this.checkAutoUpgrade(teamStats)
this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`) this.logger.log(`[PLANTING] Completed processing tree planted event for user ${userId}`)
} catch (error) { } catch (error) {
this.logger.error(`[PLANTING] Error processing tree planted for user ${userId}:`, 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) await this.eventPublisher.publishAll(assessment.domainEvents)
assessment.clearDomainEvents() 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<void> {
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<void> {
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<void> {
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}`)
}
} }

View File

@ -1,436 +1,437 @@
import { Test, TestingModule } from '@nestjs/testing' import { Test, TestingModule } from '@nestjs/testing'
import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service' import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service'
import { AssessmentCalculatorService, TeamStatistics, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service' import { AssessmentCalculatorService, TeamStatistics, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service'
import { IAuthorizationRoleRepository } from '@/domain/repositories/authorization-role.repository' import { IAuthorizationRoleRepository } from '@/domain/repositories/authorization-role.repository'
import { IMonthlyAssessmentRepository } from '@/domain/repositories/monthly-assessment.repository' import { IMonthlyAssessmentRepository } from '@/domain/repositories/monthly-assessment.repository'
import { AUTHORIZATION_ROLE_REPOSITORY } from '@/infrastructure/persistence/repositories/authorization-role.repository.impl' 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 { MONTHLY_ASSESSMENT_REPOSITORY } from '@/infrastructure/persistence/repositories/monthly-assessment.repository.impl'
import { UserId, RegionCode, Month, AuthorizationId } from '@/domain/value-objects' import { UserId, RegionCode, Month, AuthorizationId } from '@/domain/value-objects'
import { RoleType, AuthorizationStatus } from '@/domain/enums' import { RoleType, AuthorizationStatus } from '@/domain/enums'
import { AuthorizationRole } from '@/domain/aggregates' import { AuthorizationRole } from '@/domain/aggregates'
import { LadderTargetRule } from '@/domain/entities' import { LadderTargetRule } from '@/domain/entities'
describe('Domain Services Integration Tests', () => { describe('Domain Services Integration Tests', () => {
let module: TestingModule let module: TestingModule
let validatorService: AuthorizationValidatorService let validatorService: AuthorizationValidatorService
let calculatorService: AssessmentCalculatorService let calculatorService: AssessmentCalculatorService
// Mock repositories // Mock repositories
const mockAuthorizationRoleRepository: jest.Mocked<IAuthorizationRoleRepository> = { const mockAuthorizationRoleRepository: jest.Mocked<IAuthorizationRoleRepository> = {
save: jest.fn(), save: jest.fn(),
findById: jest.fn(), findById: jest.fn(),
findByUserIdAndRoleType: jest.fn(), findByUserIdAndRoleType: jest.fn(),
findByUserIdRoleTypeAndRegion: jest.fn(), findByUserIdRoleTypeAndRegion: jest.fn(),
findByUserId: jest.fn(), findByUserId: jest.fn(),
findActiveByRoleTypeAndRegion: jest.fn(), findActiveByRoleTypeAndRegion: jest.fn(),
findAllActive: jest.fn(), findAllActive: jest.fn(),
findPendingByUserId: jest.fn(), findPendingByUserId: jest.fn(),
findByStatus: jest.fn(), findByStatus: jest.fn(),
delete: jest.fn(), delete: jest.fn(),
findByAccountSequenceAndRoleType: jest.fn(), findByAccountSequenceAndRoleType: jest.fn(),
findByAccountSequence: jest.fn(), findByAccountSequence: jest.fn(),
findActiveCommunityByAccountSequences: jest.fn(), findActiveCommunityByAccountSequences: jest.fn(),
findActiveProvinceByAccountSequencesAndRegion: jest.fn(), findActiveProvinceByAccountSequencesAndRegion: jest.fn(),
findActiveCityByAccountSequencesAndRegion: jest.fn(), findActiveCityByAccountSequencesAndRegion: jest.fn(),
findCommunityByAccountSequences: jest.fn(), findCommunityByAccountSequences: jest.fn(),
findAuthProvinceByAccountSequencesAndRegion: jest.fn(), findAuthProvinceByAccountSequencesAndRegion: jest.fn(),
findAuthCityByAccountSequencesAndRegion: jest.fn(), findAuthCityByAccountSequencesAndRegion: jest.fn(),
findAuthProvinceByAccountSequences: jest.fn(), findAuthProvinceByAccountSequences: jest.fn(),
findAuthCityByAccountSequences: jest.fn(), findAuthCityByAccountSequences: jest.fn(),
findProvinceCompanyByRegion: jest.fn(), findProvinceCompanyByRegion: jest.fn(),
findCityCompanyByRegion: jest.fn(), findCityCompanyByRegion: jest.fn(),
findExpiredActiveByRoleType: jest.fn(), findExpiredActiveByRoleType: jest.fn(),
findCommunityByName: jest.fn(), findCommunityByName: jest.fn(),
findAllByUserIdIncludeDeleted: jest.fn(), findAllByUserIdIncludeDeleted: jest.fn(),
findAllByAccountSequenceIncludeDeleted: jest.fn(), findAllByAccountSequenceIncludeDeleted: jest.fn(),
findByIdIncludeDeleted: jest.fn(), findByIdIncludeDeleted: jest.fn(),
} }
const mockMonthlyAssessmentRepository: jest.Mocked<IMonthlyAssessmentRepository> = { const mockMonthlyAssessmentRepository: jest.Mocked<IMonthlyAssessmentRepository> = {
save: jest.fn(), save: jest.fn(),
saveAll: jest.fn(), saveAll: jest.fn(),
findById: jest.fn(), findById: jest.fn(),
findByAuthorizationAndMonth: jest.fn(), findByAuthorizationAndMonth: jest.fn(),
findByUserAndMonth: jest.fn(), findByUserAndMonth: jest.fn(),
findFirstByAuthorization: jest.fn(), findFirstByAuthorization: jest.fn(),
findByMonthAndRegion: jest.fn(), findByMonthAndRegion: jest.fn(),
findRankingsByMonthAndRegion: jest.fn(), findRankingsByMonthAndRegion: jest.fn(),
findByAuthorization: jest.fn(), findByAuthorization: jest.fn(),
delete: jest.fn(), delete: jest.fn(),
} }
// Mock referral repository // Mock referral repository
const mockReferralRepository: jest.Mocked<IReferralRepository> = { const mockReferralRepository: jest.Mocked<IReferralRepository> = {
findByUserId: jest.fn(), findByUserId: jest.fn(),
getAllAncestors: jest.fn(), getAllAncestors: jest.fn(),
getAllDescendants: jest.fn(), getAllDescendants: jest.fn(),
} }
// Mock team statistics repository // Mock team statistics repository
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = { const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
findByUserId: jest.fn(), findByUserId: jest.fn(),
findByAccountSequence: jest.fn(), findByAccountSequence: jest.fn(),
} getReferralChain: jest.fn(),
}
beforeAll(async () => {
module = await Test.createTestingModule({ beforeAll(async () => {
providers: [ module = await Test.createTestingModule({
AuthorizationValidatorService, providers: [
AssessmentCalculatorService, AuthorizationValidatorService,
{ AssessmentCalculatorService,
provide: AUTHORIZATION_ROLE_REPOSITORY, {
useValue: mockAuthorizationRoleRepository, provide: AUTHORIZATION_ROLE_REPOSITORY,
}, useValue: mockAuthorizationRoleRepository,
{ },
provide: MONTHLY_ASSESSMENT_REPOSITORY, {
useValue: mockMonthlyAssessmentRepository, provide: MONTHLY_ASSESSMENT_REPOSITORY,
}, useValue: mockMonthlyAssessmentRepository,
{ },
provide: 'REFERRAL_REPOSITORY', {
useValue: mockReferralRepository, provide: 'REFERRAL_REPOSITORY',
}, useValue: mockReferralRepository,
{ },
provide: 'TEAM_STATISTICS_REPOSITORY', {
useValue: mockTeamStatisticsRepository, provide: 'TEAM_STATISTICS_REPOSITORY',
}, useValue: mockTeamStatisticsRepository,
], },
}).compile() ],
}).compile()
validatorService = module.get<AuthorizationValidatorService>(AuthorizationValidatorService)
calculatorService = module.get<AssessmentCalculatorService>(AssessmentCalculatorService) validatorService = module.get<AuthorizationValidatorService>(AuthorizationValidatorService)
}) calculatorService = module.get<AssessmentCalculatorService>(AssessmentCalculatorService)
})
afterAll(async () => {
await module.close() afterAll(async () => {
}) await module.close()
})
beforeEach(() => {
jest.clearAllMocks() beforeEach(() => {
}) jest.clearAllMocks()
})
describe('AuthorizationValidatorService', () => {
describe('validateAuthorizationRequest', () => { describe('AuthorizationValidatorService', () => {
it('should return success when no conflicts in referral chain', async () => { describe('validateAuthorizationRequest', () => {
const userId = UserId.create('user-123', 'D2412190123') it('should return success when no conflicts in referral chain', async () => {
const roleType = RoleType.AUTH_PROVINCE_COMPANY const userId = UserId.create('user-123', 'D2412190123')
const regionCode = RegionCode.create('430000') const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.findByUserId.mockResolvedValue(null) mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.findByUserId.mockResolvedValue(null)
const result = await validatorService.validateAuthorizationRequest(
userId, const result = await validatorService.validateAuthorizationRequest(
roleType, userId,
regionCode, roleType,
mockReferralRepository, regionCode,
mockAuthorizationRoleRepository, mockReferralRepository,
) mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(true)
}) expect(result.isValid).toBe(true)
})
it('should return failure when user already has province authorization', async () => {
const userId = UserId.create('user-123', 'D2412190123') it('should return failure when user already has province authorization', async () => {
const roleType = RoleType.AUTH_PROVINCE_COMPANY const userId = UserId.create('user-123', 'D2412190123')
const regionCode = RegionCode.create('430000') const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
const existingAuth = AuthorizationRole.createAuthProvinceCompany({
userId, const existingAuth = AuthorizationRole.createAuthProvinceCompany({
provinceCode: '440000', userId,
provinceName: '广东省', provinceCode: '440000',
}) provinceName: '广东省',
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth) })
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth)
const result = await validatorService.validateAuthorizationRequest(
userId, const result = await validatorService.validateAuthorizationRequest(
roleType, userId,
regionCode, roleType,
mockReferralRepository, regionCode,
mockAuthorizationRoleRepository, mockReferralRepository,
) mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(false)
expect(result.errorMessage).toContain('只能申请一个省代或市代授权') 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') it('should return failure when ancestor has same region authorization', async () => {
const roleType = RoleType.AUTH_PROVINCE_COMPANY const userId = UserId.create('user-123', 'D2412190123')
const regionCode = RegionCode.create('430000') const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
const ancestorUserId = UserId.create('ancestor-user', 'D2412190999')
const ancestorUserId = UserId.create('ancestor-user', 'D2412190999')
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' }) mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId]) mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' })
mockReferralRepository.getAllDescendants.mockResolvedValue([]) mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId])
mockReferralRepository.getAllDescendants.mockResolvedValue([])
const existingAuth = AuthorizationRole.createAuthProvinceCompany({
userId: ancestorUserId, const existingAuth = AuthorizationRole.createAuthProvinceCompany({
provinceCode: '430000', userId: ancestorUserId,
provinceName: '湖南省', provinceCode: '430000',
}) provinceName: '湖南省',
mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth) })
mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth)
const result = await validatorService.validateAuthorizationRequest(
userId, const result = await validatorService.validateAuthorizationRequest(
roleType, userId,
regionCode, roleType,
mockReferralRepository, regionCode,
mockAuthorizationRoleRepository, mockReferralRepository,
) mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(false)
expect(result.errorMessage).toContain('本团队已有人申请') expect(result.isValid).toBe(false)
}) expect(result.errorMessage).toContain('本团队已有人申请')
}) })
}) })
})
describe('AssessmentCalculatorService', () => {
describe('assessAndRankRegion', () => { describe('AssessmentCalculatorService', () => {
it('should calculate assessments and rank by exceed ratio', async () => { describe('assessAndRankRegion', () => {
const roleType = RoleType.AUTH_PROVINCE_COMPANY it('should calculate assessments and rank by exceed ratio', async () => {
const regionCode = RegionCode.create('430000') const roleType = RoleType.AUTH_PROVINCE_COMPANY
const assessmentMonth = Month.current() const regionCode = RegionCode.create('430000')
const assessmentMonth = Month.current()
// Mock two authorizations
const auth1 = AuthorizationRole.createAuthProvinceCompany({ // Mock two authorizations
userId: UserId.create('user-1', 'D2412190001'), const auth1 = AuthorizationRole.createAuthProvinceCompany({
provinceCode: '430000', userId: UserId.create('user-1', 'D2412190001'),
provinceName: '湖南省', provinceCode: '430000',
}) provinceName: '湖南省',
auth1.authorize(UserId.create('admin', 'D2412190100')) })
auth1.authorize(UserId.create('admin', 'D2412190100'))
const auth2 = AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-2', 'D2412190002'), const auth2 = AuthorizationRole.createAuthProvinceCompany({
provinceCode: '430000', userId: UserId.create('user-2', 'D2412190002'),
provinceName: '湖南省', provinceCode: '430000',
}) provinceName: '湖南省',
auth2.authorize(UserId.create('admin', 'D2412190100')) })
auth2.authorize(UserId.create('admin', 'D2412190100'))
mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2])
mockMonthlyAssessmentRepository.findByAuthorizationAndMonth.mockResolvedValue(null) mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2])
mockMonthlyAssessmentRepository.findByAuthorizationAndMonth.mockResolvedValue(null)
// User 1 has better stats
mockTeamStatisticsRepository.findByUserId // User 1 has better stats
.mockResolvedValueOnce({ mockTeamStatisticsRepository.findByUserId
userId: 'user-1', .mockResolvedValueOnce({
accountSequence: 'D2412190001', userId: 'user-1',
totalTeamPlantingCount: 200, accountSequence: 'D2412190001',
selfPlantingCount: 10, totalTeamPlantingCount: 200,
get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, selfPlantingCount: 10,
getProvinceTeamCount: () => 70, get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount },
getCityTeamCount: () => 0, getProvinceTeamCount: () => 70,
}) getCityTeamCount: () => 0,
.mockResolvedValueOnce({ })
userId: 'user-2', .mockResolvedValueOnce({
accountSequence: 'D2412190002', userId: 'user-2',
totalTeamPlantingCount: 100, accountSequence: 'D2412190002',
selfPlantingCount: 5, totalTeamPlantingCount: 100,
get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount }, selfPlantingCount: 5,
getProvinceTeamCount: () => 35, get subordinateTeamPlantingCount() { return this.totalTeamPlantingCount - this.selfPlantingCount },
getCityTeamCount: () => 0, getProvinceTeamCount: () => 35,
}) getCityTeamCount: () => 0,
})
const assessments = await calculatorService.assessAndRankRegion(
roleType, const assessments = await calculatorService.assessAndRankRegion(
regionCode, roleType,
assessmentMonth, regionCode,
mockAuthorizationRoleRepository, assessmentMonth,
mockTeamStatisticsRepository, mockAuthorizationRoleRepository,
mockMonthlyAssessmentRepository, mockTeamStatisticsRepository,
) mockMonthlyAssessmentRepository,
)
expect(assessments.length).toBe(2)
// User 1 should rank first (higher exceed ratio) expect(assessments.length).toBe(2)
expect(assessments[0].userId.value).toBe('user-1') // User 1 should rank first (higher exceed ratio)
expect(assessments[0].rankingInRegion).toBe(1) expect(assessments[0].userId.value).toBe('user-1')
expect(assessments[0].isFirstPlace).toBe(true) expect(assessments[0].rankingInRegion).toBe(1)
}) expect(assessments[0].isFirstPlace).toBe(true)
}) })
}) })
}) })
})
describe('LadderTargetRule Entity Integration Tests', () => {
describe('getTarget', () => { describe('LadderTargetRule Entity Integration Tests', () => {
it('should return correct target for province month 1', () => { describe('getTarget', () => {
const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, 1) 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) 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) 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) 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) 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) 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) 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) 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) 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) expect(target.monthlyTarget).toBe(2350)
}) expect(target.cumulativeTarget).toBe(10000)
})
it('should return fixed target for community', () => {
const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1) it('should return fixed target for community', () => {
const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1)
expect(target.monthlyTarget).toBe(10)
expect(target.cumulativeTarget).toBe(10) expect(target.monthlyTarget).toBe(10)
}) expect(target.cumulativeTarget).toBe(10)
}) })
})
describe('getFinalTarget', () => {
it('should return 50000 for province', () => { describe('getFinalTarget', () => {
expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_PROVINCE_COMPANY)).toBe(50000) 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 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) it('should return 10 for community', () => {
}) expect(LadderTargetRule.getFinalTarget(RoleType.COMMUNITY)).toBe(10)
}) })
})
describe('getAllTargets', () => {
it('should return 9 targets for province', () => { describe('getAllTargets', () => {
const targets = LadderTargetRule.getAllTargets(RoleType.AUTH_PROVINCE_COMPANY) it('should return 9 targets for province', () => {
expect(targets.length).toBe(9) 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) it('should return 9 targets for city', () => {
expect(targets.length).toBe(9) 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) it('should return 1 target for community', () => {
expect(targets.length).toBe(1) const targets = LadderTargetRule.getAllTargets(RoleType.COMMUNITY)
}) expect(targets.length).toBe(1)
}) })
}) })
})
describe('Month Value Object Integration Tests', () => {
describe('create and format', () => { describe('Month Value Object Integration Tests', () => {
it('should create month from string', () => { describe('create and format', () => {
const month = Month.create('2024-03') it('should create month from string', () => {
expect(month.value).toBe('2024-03') 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') it('should create month from date', () => {
const month = Month.fromDate(date) const date = new Date('2024-06-15')
expect(month.value).toBe('2024-06') const month = Month.fromDate(date)
}) expect(month.value).toBe('2024-06')
})
it('should create current month', () => {
const month = Month.current() it('should create current month', () => {
const now = new Date() const month = Month.current()
const expectedMonth = String(now.getMonth() + 1).padStart(2, '0') const now = new Date()
const expectedYear = now.getFullYear() const expectedMonth = String(now.getMonth() + 1).padStart(2, '0')
expect(month.value).toBe(`${expectedYear}-${expectedMonth}`) const expectedYear = now.getFullYear()
}) expect(month.value).toBe(`${expectedYear}-${expectedMonth}`)
}) })
})
describe('navigation', () => {
it('should get next month correctly', () => { describe('navigation', () => {
const month = Month.create('2024-03') it('should get next month correctly', () => {
const next = month.next() const month = Month.create('2024-03')
expect(next.value).toBe('2024-04') const next = month.next()
}) expect(next.value).toBe('2024-04')
})
it('should handle year boundary for next', () => {
const month = Month.create('2024-12') it('should handle year boundary for next', () => {
const next = month.next() const month = Month.create('2024-12')
expect(next.value).toBe('2025-01') const next = month.next()
}) expect(next.value).toBe('2025-01')
})
it('should get previous month correctly', () => {
const month = Month.create('2024-03') it('should get previous month correctly', () => {
const prev = month.previous() const month = Month.create('2024-03')
expect(prev.value).toBe('2024-02') const prev = month.previous()
}) expect(prev.value).toBe('2024-02')
})
it('should handle year boundary for previous', () => {
const month = Month.create('2024-01') it('should handle year boundary for previous', () => {
const prev = month.previous() const month = Month.create('2024-01')
expect(prev.value).toBe('2023-12') const prev = month.previous()
}) expect(prev.value).toBe('2023-12')
}) })
})
describe('comparison', () => {
it('should compare months correctly', () => { describe('comparison', () => {
const month1 = Month.create('2024-03') it('should compare months correctly', () => {
const month2 = Month.create('2024-06') 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.isBefore(month2)).toBe(true)
expect(month1.equals(month1)).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') it('should compare across years', () => {
const month2 = Month.create('2024-01') const month1 = Month.create('2023-12')
const month2 = Month.create('2024-01')
expect(month1.isBefore(month2)).toBe(true)
expect(month2.isAfter(month1)).toBe(true) expect(month1.isBefore(month2)).toBe(true)
}) expect(month2.isAfter(month1)).toBe(true)
}) })
})
describe('extraction', () => {
it('should extract year and month', () => { describe('extraction', () => {
const month = Month.create('2024-03') it('should extract year and month', () => {
const month = Month.create('2024-03')
expect(month.getYear()).toBe(2024)
expect(month.getMonth()).toBe(3) expect(month.getYear()).toBe(2024)
}) expect(month.getMonth()).toBe(3)
}) })
}) })
})
describe('RegionCode Value Object Integration Tests', () => {
describe('create', () => { describe('RegionCode Value Object Integration Tests', () => {
it('should create valid region code', () => { describe('create', () => {
const regionCode = RegionCode.create('430000') it('should create valid region code', () => {
expect(regionCode.value).toBe('430000') const regionCode = RegionCode.create('430000')
}) expect(regionCode.value).toBe('430000')
})
it('should create city code', () => {
const regionCode = RegionCode.create('430100') it('should create city code', () => {
expect(regionCode.value).toBe('430100') const regionCode = RegionCode.create('430100')
}) expect(regionCode.value).toBe('430100')
}) })
})
describe('equality', () => {
it('should compare equal region codes', () => { describe('equality', () => {
const code1 = RegionCode.create('430000') it('should compare equal region codes', () => {
const code2 = RegionCode.create('430000') const code1 = RegionCode.create('430000')
const code2 = RegionCode.create('430000')
expect(code1.equals(code2)).toBe(true)
}) expect(code1.equals(code2)).toBe(true)
})
it('should compare different region codes', () => {
const code1 = RegionCode.create('430000') it('should compare different region codes', () => {
const code2 = RegionCode.create('440000') const code1 = RegionCode.create('430000')
const code2 = RegionCode.create('440000')
expect(code1.equals(code2)).toBe(false)
}) expect(code1.equals(code2)).toBe(false)
}) })
}) })
})