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:
parent
0506a3547c
commit
7a264a0158
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue