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
}
// 工厂方法 - 自动升级创建省区域省团队伞下第一个累计达到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

View File

@ -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

View File

@ -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<TeamStatistics | null>
findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
}
export class AssessmentCalculatorService {
/**
*
*/
async calculateMonthlyAssessment(
authorization: AuthorizationRole,
assessmentMonth: Month,
teamStats: TeamStatistics,
repository: IMonthlyAssessmentRepository,
): Promise<MonthlyAssessment> {
// 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<MonthlyAssessment[]> {
// 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<TeamStatistics | null>
findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
/** 获取用户的祖先链(推荐链),返回从直接推荐人到根节点的 accountSequence 列表 */
getReferralChain(accountSequence: string): Promise<string[]>
}
export class AssessmentCalculatorService {
/**
*
*/
async calculateMonthlyAssessment(
authorization: AuthorizationRole,
assessmentMonth: Month,
teamStats: TeamStatistics,
repository: IMonthlyAssessmentRepository,
): Promise<MonthlyAssessment> {
// 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<MonthlyAssessment[]> {
// 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
}
}

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}`)
} 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<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 { 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<IAuthorizationRoleRepository> = {
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<IMonthlyAssessmentRepository> = {
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<IReferralRepository> = {
findByUserId: jest.fn(),
getAllAncestors: jest.fn(),
getAllDescendants: jest.fn(),
}
// Mock team statistics repository
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
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>(AuthorizationValidatorService)
calculatorService = module.get<AssessmentCalculatorService>(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<IAuthorizationRoleRepository> = {
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<IMonthlyAssessmentRepository> = {
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<IReferralRepository> = {
findByUserId: jest.fn(),
getAllAncestors: jest.fn(),
getAllDescendants: jest.fn(),
}
// Mock team statistics repository
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
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>(AuthorizationValidatorService)
calculatorService = module.get<AssessmentCalculatorService>(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)
})
})
})