diff --git a/backend/services/contribution-service/prisma/schema.prisma b/backend/services/contribution-service/prisma/schema.prisma index 84810b53..c9071b52 100644 --- a/backend/services/contribution-service/prisma/schema.prisma +++ b/backend/services/contribution-service/prisma/schema.prisma @@ -207,6 +207,9 @@ model ContributionRecord { isExpired Boolean @default(false) @map("is_expired") expiredAt DateTime? @map("expired_at") + // ========== 转让关联(纯新增,可空字段)========== + transferOrderNo String? @map("transfer_order_no") @db.VarChar(50) // 关联转让订单号(仅转让产生的记录有值) + // ========== 备注 ========== remark String? @map("remark") @db.VarChar(500) // 备注说明 @@ -220,6 +223,7 @@ model ContributionRecord { @@index([status]) @@index([expireDate]) @@index([isExpired]) + @@index([transferOrderNo]) @@map("contribution_records") } @@ -279,6 +283,9 @@ model UnallocatedContribution { amount Decimal @map("amount") @db.Decimal(30, 10) reason String? @db.VarChar(200) // 未分配原因 + // ========== 转让关联(纯新增,可空字段)========== + transferOrderNo String? @map("transfer_order_no") @db.VarChar(50) // 关联转让订单号 + // ========== 分配状态 ========== status String @default("PENDING") @map("status") @db.VarChar(20) // PENDING / ALLOCATED_TO_USER / ALLOCATED_TO_HQ allocatedAt DateTime? @map("allocated_at") @@ -294,6 +301,7 @@ model UnallocatedContribution { @@index([wouldBeAccountSequence]) @@index([unallocType]) @@index([status]) + @@index([transferOrderNo]) @@map("unallocated_contributions") } @@ -335,6 +343,9 @@ model SystemContributionRecord { distributionRate Decimal @map("distribution_rate") @db.Decimal(10, 6) amount Decimal @map("amount") @db.Decimal(30, 10) + // 转让关联(纯新增,可空字段) + transferOrderNo String? @map("transfer_order_no") @db.VarChar(50) + effectiveDate DateTime @map("effective_date") @db.Date expireDate DateTime? @map("expire_date") @db.Date isExpired Boolean @default(false) @map("is_expired") @@ -348,6 +359,7 @@ model SystemContributionRecord { @@index([sourceAdoptionId]) @@index([deletedAt]) @@index([sourceType]) + @@index([transferOrderNo]) @@map("system_contribution_records") } diff --git a/backend/services/contribution-service/src/application/application.module.ts b/backend/services/contribution-service/src/application/application.module.ts index 654da44b..f68b90bd 100644 --- a/backend/services/contribution-service/src/application/application.module.ts +++ b/backend/services/contribution-service/src/application/application.module.ts @@ -7,6 +7,8 @@ import { UserSyncedHandler } from './event-handlers/user-synced.handler'; import { ReferralSyncedHandler } from './event-handlers/referral-synced.handler'; import { AdoptionSyncedHandler } from './event-handlers/adoption-synced.handler'; import { CDCEventDispatcher } from './event-handlers/cdc-event-dispatcher'; +// [2026-02-19] 纯新增:转让所有权事件处理器 +import { TransferOwnershipHandler } from './event-handlers/transfer-ownership.handler'; // Services import { ContributionCalculationService } from './services/contribution-calculation.service'; @@ -14,6 +16,8 @@ import { ContributionDistributionPublisherService } from './services/contributio import { ContributionRateService } from './services/contribution-rate.service'; import { BonusClaimService } from './services/bonus-claim.service'; import { SnapshotService } from './services/snapshot.service'; +// [2026-02-19] 纯新增:转让算力调整服务 +import { TransferAdjustmentService } from './services/transfer-adjustment.service'; // Queries import { GetContributionAccountQuery } from './queries/get-contribution-account.query'; @@ -36,6 +40,7 @@ import { ContributionScheduler } from './schedulers/contribution.scheduler'; ReferralSyncedHandler, AdoptionSyncedHandler, CDCEventDispatcher, + TransferOwnershipHandler, // [2026-02-19] 纯新增:转让所有权事件处理器 // Services ContributionCalculationService, @@ -43,6 +48,7 @@ import { ContributionScheduler } from './schedulers/contribution.scheduler'; ContributionRateService, BonusClaimService, SnapshotService, + TransferAdjustmentService, // [2026-02-19] 纯新增:转让算力调整服务 // Queries GetContributionAccountQuery, diff --git a/backend/services/contribution-service/src/application/event-handlers/transfer-ownership.handler.ts b/backend/services/contribution-service/src/application/event-handlers/transfer-ownership.handler.ts new file mode 100644 index 00000000..21a747ef --- /dev/null +++ b/backend/services/contribution-service/src/application/event-handlers/transfer-ownership.handler.ts @@ -0,0 +1,217 @@ +/** + * 转让所有权事件处理器(纯新增) + * 消费 planting.ownership.removed + planting.ownership.added 事件 + * 调用 TransferAdjustmentService 执行 88% 算力调整 + * + * 消费模式:kafkajs 原生消费者(与 CDCConsumerService 同架构) + * 幂等性:ProcessedEvent 表(sourceService + eventId) + * + * 回滚方式:删除此文件并从 application.module.ts 中移除引用 + */ + +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Kafka, Consumer, EachMessagePayload } from 'kafkajs'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; +import { + TransferAdjustmentService, + OwnershipRemovedEvent, + OwnershipAddedEvent, +} from '../services/transfer-adjustment.service'; + +@Injectable() +export class TransferOwnershipHandler implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(TransferOwnershipHandler.name); + private kafka: Kafka; + private removedConsumer: Consumer; + private addedConsumer: Consumer; + + constructor( + private readonly configService: ConfigService, + private readonly prisma: PrismaService, + private readonly transferAdjustmentService: TransferAdjustmentService, + ) { + const brokers = this.configService + .get('KAFKA_BROKERS', 'localhost:9092') + .split(','); + + this.kafka = new Kafka({ + clientId: 'contribution-service-transfer', + brokers, + }); + + this.removedConsumer = this.kafka.consumer({ + groupId: 'contribution-service-transfer-removed', + }); + + this.addedConsumer = this.kafka.consumer({ + groupId: 'contribution-service-transfer-added', + }); + } + + async onModuleInit(): Promise { + try { + // 订阅卖方减持事件 + await this.removedConsumer.connect(); + await this.removedConsumer.subscribe({ + topic: 'planting.ownership.removed', + fromBeginning: false, + }); + await this.removedConsumer.run({ + eachMessage: async (payload) => { + await this.handleOwnershipRemoved(payload); + }, + }); + + // 订阅买方增持事件 + await this.addedConsumer.connect(); + await this.addedConsumer.subscribe({ + topic: 'planting.ownership.added', + fromBeginning: false, + }); + await this.addedConsumer.run({ + eachMessage: async (payload) => { + await this.handleOwnershipAdded(payload); + }, + }); + + this.logger.log( + 'Subscribed to planting.ownership.removed + planting.ownership.added', + ); + } catch (error) { + this.logger.error('Failed to initialize transfer ownership consumers', error); + } + } + + async onModuleDestroy(): Promise { + try { + await this.removedConsumer.disconnect(); + await this.addedConsumer.disconnect(); + } catch (error) { + this.logger.error('Failed to disconnect transfer consumers', error); + } + } + + /** + * 处理卖方减持事件 + */ + private async handleOwnershipRemoved(payload: EachMessagePayload): Promise { + const { message } = payload; + if (!message.value) return; + + let event: OwnershipRemovedEvent; + try { + const raw = JSON.parse(message.value.toString()); + // 支持两种消息格式:直接 payload 或嵌套在 value 字段中 + event = raw.value ? raw.value : raw; + } catch (error) { + this.logger.error('[TRANSFER-REMOVED] Failed to parse message', error); + return; + } + + const processedEventId = `removed:${event.transferOrderNo}`; + + this.logger.log( + `[TRANSFER-REMOVED] Processing: ${event.transferOrderNo}, ` + + `seller=${event.sellerAccountSequence}, trees=${event.treeCount}`, + ); + + try { + // 幂等性检查 + const existing = await this.prisma.processedEvent.findFirst({ + where: { + sourceService: 'planting-service', + eventId: processedEventId, + }, + }); + + if (existing) { + this.logger.log(`[TRANSFER-REMOVED] Already processed: ${processedEventId}`); + return; + } + + // 执行卖方算力扣减 + await this.transferAdjustmentService.processOwnershipRemoved(event); + + // 记录已处理 + await this.prisma.processedEvent.create({ + data: { + sourceService: 'planting-service', + eventId: processedEventId, + eventType: 'PlantingOwnershipRemoved', + }, + }); + + this.logger.log( + `[TRANSFER-REMOVED] ✓ Processed: ${event.transferOrderNo}, seller=${event.sellerAccountSequence}`, + ); + } catch (error) { + this.logger.error( + `[TRANSFER-REMOVED] ✗ Failed for ${event.transferOrderNo}:`, + error, + ); + throw error; // 让 kafkajs 重试 + } + } + + /** + * 处理买方增持事件 + */ + private async handleOwnershipAdded(payload: EachMessagePayload): Promise { + const { message } = payload; + if (!message.value) return; + + let event: OwnershipAddedEvent; + try { + const raw = JSON.parse(message.value.toString()); + event = raw.value ? raw.value : raw; + } catch (error) { + this.logger.error('[TRANSFER-ADDED] Failed to parse message', error); + return; + } + + const processedEventId = `added:${event.transferOrderNo}`; + + this.logger.log( + `[TRANSFER-ADDED] Processing: ${event.transferOrderNo}, ` + + `buyer=${event.buyerAccountSequence}, trees=${event.treeCount}`, + ); + + try { + // 幂等性检查 + const existing = await this.prisma.processedEvent.findFirst({ + where: { + sourceService: 'planting-service', + eventId: processedEventId, + }, + }); + + if (existing) { + this.logger.log(`[TRANSFER-ADDED] Already processed: ${processedEventId}`); + return; + } + + // 执行买方算力新增 + await this.transferAdjustmentService.processOwnershipAdded(event); + + // 记录已处理 + await this.prisma.processedEvent.create({ + data: { + sourceService: 'planting-service', + eventId: processedEventId, + eventType: 'PlantingOwnershipAdded', + }, + }); + + this.logger.log( + `[TRANSFER-ADDED] ✓ Processed: ${event.transferOrderNo}, buyer=${event.buyerAccountSequence}`, + ); + } catch (error) { + this.logger.error( + `[TRANSFER-ADDED] ✗ Failed for ${event.transferOrderNo}:`, + error, + ); + throw error; // 让 kafkajs 重试 + } + } +} diff --git a/backend/services/contribution-service/src/application/services/transfer-adjustment.service.ts b/backend/services/contribution-service/src/application/services/transfer-adjustment.service.ts new file mode 100644 index 00000000..8ddd7ffc --- /dev/null +++ b/backend/services/contribution-service/src/application/services/transfer-adjustment.service.ts @@ -0,0 +1,879 @@ +/** + * 转让算力调整服务(纯新增) + * 处理 planting.ownership.removed 和 planting.ownership.added 事件 + * 对受影响的 88% 算力(个人70% + 团队层级7.5% + 个人加成7.5% + 省1% + 市2%)进行对冲/新增 + * + * 设计说明: + * - 直接使用 Prisma 操作数据库,不经过聚合根(因为转让涉及负数金额,而 ContributionAmount 不支持负数) + * - 所有调整通过追加新流水记录实现(对冲式),历史记录零修改、零删除 + * - 运营账户 12% 完全不受影响 + * + * 回滚方式:删除此文件并从 application.module.ts 中移除引用 + */ + +import { Injectable, Logger } from '@nestjs/common'; +import Decimal from 'decimal.js'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; +import { ContributionAccountRepository } from '../../infrastructure/persistence/repositories/contribution-account.repository'; +import { SyncedDataRepository } from '../../infrastructure/persistence/repositories/synced-data.repository'; +import { SystemAccountRepository } from '../../infrastructure/persistence/repositories/system-account.repository'; +import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository'; +import { UnitOfWork } from '../../infrastructure/persistence/unit-of-work/unit-of-work'; +import { ContributionAmount } from '../../domain/value-objects/contribution-amount.vo'; +import { BonusClaimService } from './bonus-claim.service'; + +/** 转让事件通用字段 */ +interface TransferEventBase { + transferOrderNo: string; + sourceOrderNo: string; + sourceOrderId: string; // bigint → string + treeCount: number; + contributionPerTree: string; + selectedProvince: string; + selectedCity: string; + originalAdoptionDate: string; + originalExpireDate: string; +} + +/** 卖方减持事件 */ +export interface OwnershipRemovedEvent extends TransferEventBase { + sellerUserId: string; + sellerAccountSequence: string; +} + +/** 买方增持事件 */ +export interface OwnershipAddedEvent extends TransferEventBase { + buyerUserId: string; + buyerAccountSequence: string; +} + +// 分配比例常量 +const RATE_PERSONAL = new Decimal('0.70'); +const RATE_LEVEL_PER = new Decimal('0.005'); +const RATE_BONUS_PER = new Decimal('0.025'); +const RATE_PROVINCE = new Decimal('0.01'); +const RATE_CITY = new Decimal('0.02'); + +@Injectable() +export class TransferAdjustmentService { + private readonly logger = new Logger(TransferAdjustmentService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly contributionAccountRepository: ContributionAccountRepository, + private readonly syncedDataRepository: SyncedDataRepository, + private readonly systemAccountRepository: SystemAccountRepository, + private readonly outboxRepository: OutboxRepository, + private readonly unitOfWork: UnitOfWork, + private readonly bonusClaimService: BonusClaimService, + ) {} + + /** + * 处理卖方减持(planting.ownership.removed) + * 创建负数对冲流水,扣减卖方及其上线链路的算力 + */ + async processOwnershipRemoved(event: OwnershipRemovedEvent): Promise { + const sourceAdoptionId = BigInt(event.sourceOrderId); + const baseContribution = new Decimal(event.contributionPerTree); + const totalContribution = baseContribution.times(event.treeCount); + const transferDate = new Date(); + const expireDate = new Date(event.originalExpireDate); + + this.logger.log( + `[TRANSFER-OUT] Starting: ${event.transferOrderNo}, seller=${event.sellerAccountSequence}, ` + + `trees=${event.treeCount}, contribution/tree=${event.contributionPerTree}`, + ); + + await this.unitOfWork.executeInTransaction(async () => { + // 1. 卖方个人算力扣减(70%) + const personalAmount = totalContribution.times(RATE_PERSONAL); + await this.createTransferRecord({ + accountSequence: event.sellerAccountSequence, + sourceType: 'TRANSFER_OUT_PERSONAL', + sourceAdoptionId, + sourceAccountSequence: event.sellerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_PERSONAL, + amount: personalAmount.negated(), // 负数 + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转出个人算力`, + }); + await this.decrementAccountEffective(event.sellerAccountSequence, personalAmount); + await this.decrementPersonalContribution(event.sellerAccountSequence, personalAmount); + + // 2. 卖方上线团队层级扣减(每级0.5%,最多15级) + const sellerReferral = await this.syncedDataRepository.findSyncedReferralByAccountSequence( + event.sellerAccountSequence, + ); + if (sellerReferral?.referrerAccountSequence) { + const ancestorChain = await this.syncedDataRepository.findAncestorChain( + sellerReferral.referrerAccountSequence, + 15, + ); + + for (let level = 1; level <= 15; level++) { + const ancestor = ancestorChain[level - 1]; + if (!ancestor) break; + + const levelAmount = totalContribution.times(RATE_LEVEL_PER); + + // 查询该级别是否有已分配的记录 + const originalRecord = await this.prisma.contributionRecord.findFirst({ + where: { + accountSequence: ancestor.accountSequence, + sourceAdoptionId, + sourceType: 'TEAM_LEVEL', + levelDepth: level, + }, + }); + + if (originalRecord) { + // 有已分配记录 → 创建负数对冲流水 + await this.createTransferRecord({ + accountSequence: ancestor.accountSequence, + sourceType: 'TRANSFER_OUT_TEAM_LEVEL', + sourceAdoptionId, + sourceAccountSequence: event.sellerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_LEVEL_PER, + levelDepth: level, + amount: levelAmount.negated(), + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转出L${level}团队算力`, + }); + await this.decrementAccountEffective(ancestor.accountSequence, levelAmount); + await this.decrementLevelPending(ancestor.accountSequence, level, levelAmount); + } + // 如果在 UnallocatedContribution 中(上线未解锁),标记失效 + await this.invalidateUnallocatedForTransfer( + sourceAdoptionId, + event.sellerAccountSequence, + level, + event.transferOrderNo, + ); + } + } + + // 3. 卖方加成扣减(每档2.5%,最多3档) + for (let tier = 1; tier <= 3; tier++) { + const originalBonus = await this.prisma.contributionRecord.findFirst({ + where: { + accountSequence: event.sellerAccountSequence, + sourceAdoptionId, + sourceType: 'TEAM_BONUS', + bonusTier: tier, + }, + }); + + if (originalBonus) { + const bonusAmount = totalContribution.times(RATE_BONUS_PER); + await this.createTransferRecord({ + accountSequence: event.sellerAccountSequence, + sourceType: 'TRANSFER_OUT_BONUS', + sourceAdoptionId, + sourceAccountSequence: event.sellerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_BONUS_PER, + bonusTier: tier, + amount: bonusAmount.negated(), + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转出加成T${tier}`, + }); + await this.decrementAccountEffective(event.sellerAccountSequence, bonusAmount); + await this.decrementBonusPending(event.sellerAccountSequence, tier, bonusAmount); + } + + // 标记未分配的加成算力失效 + await this.invalidateUnallocatedBonusForTransfer( + sourceAdoptionId, + event.sellerAccountSequence, + tier, + event.transferOrderNo, + ); + } + + // 4. 卖方省公司系统账户扣减(1%) + const provinceAmount = totalContribution.times(RATE_PROVINCE); + await this.adjustSystemAccount( + 'PROVINCE', + event.selectedProvince, + sourceAdoptionId, + event.sellerAccountSequence, + 'TRANSFER_OUT_SYSTEM_PROVINCE', + RATE_PROVINCE, + provinceAmount.negated(), + transferDate, + expireDate, + event.transferOrderNo, + ); + + // 5. 卖方市公司系统账户扣减(2%) + const cityAmount = totalContribution.times(RATE_CITY); + await this.adjustSystemAccount( + 'CITY', + event.selectedCity, + sourceAdoptionId, + event.sellerAccountSequence, + 'TRANSFER_OUT_SYSTEM_CITY', + RATE_CITY, + cityAmount.negated(), + transferDate, + expireDate, + event.transferOrderNo, + ); + + // 6. 更新卖方解锁状态(如果卖方不再持有任何树) + await this.updateSellerUnlockStatus(event.sellerAccountSequence); + }); + + this.logger.log( + `[TRANSFER-OUT] ✓ Completed: ${event.transferOrderNo}, seller=${event.sellerAccountSequence}`, + ); + } + + /** + * 处理买方增持(planting.ownership.added) + * 创建正数流水,为买方及其上线链路新增算力 + */ + async processOwnershipAdded(event: OwnershipAddedEvent): Promise { + const sourceAdoptionId = BigInt(event.sourceOrderId); + const baseContribution = new Decimal(event.contributionPerTree); + const totalContribution = baseContribution.times(event.treeCount); + const transferDate = new Date(); + const expireDate = new Date(event.originalExpireDate); + + this.logger.log( + `[TRANSFER-IN] Starting: ${event.transferOrderNo}, buyer=${event.buyerAccountSequence}, ` + + `trees=${event.treeCount}, contribution/tree=${event.contributionPerTree}`, + ); + + await this.unitOfWork.executeInTransaction(async () => { + // 确保买方有算力账户 + let buyerAccount = await this.contributionAccountRepository.findByAccountSequence( + event.buyerAccountSequence, + ); + if (!buyerAccount) { + const { ContributionAccountAggregate } = await import( + '../../domain/aggregates/contribution-account.aggregate' + ); + buyerAccount = ContributionAccountAggregate.create(event.buyerAccountSequence); + await this.contributionAccountRepository.save(buyerAccount); + } + + // 1. 买方个人算力新增(70%) + const personalAmount = totalContribution.times(RATE_PERSONAL); + await this.createTransferRecord({ + accountSequence: event.buyerAccountSequence, + sourceType: 'TRANSFER_IN_PERSONAL', + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_PERSONAL, + amount: personalAmount, // 正数 + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转入个人算力`, + }); + await this.incrementAccountEffective(event.buyerAccountSequence, personalAmount); + await this.incrementPersonalContribution(event.buyerAccountSequence, personalAmount); + + // 2. 买方上线团队层级分配(每级0.5%,最多15级) + const buyerReferral = await this.syncedDataRepository.findSyncedReferralByAccountSequence( + event.buyerAccountSequence, + ); + if (buyerReferral?.referrerAccountSequence) { + const ancestorChain = await this.syncedDataRepository.findAncestorChain( + buyerReferral.referrerAccountSequence, + 15, + ); + const ancestorAccountSeqs = ancestorChain.map((a) => a.accountSequence); + const ancestorAccounts = await this.contributionAccountRepository.findByAccountSequences( + ancestorAccountSeqs, + ); + + for (let level = 1; level <= 15; level++) { + const ancestor = ancestorChain[level - 1]; + const levelAmount = totalContribution.times(RATE_LEVEL_PER); + + if (!ancestor) { + // 无上线 → UnallocatedContribution(归总部) + await this.createUnallocatedContribution({ + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + unallocType: 'LEVEL_NO_ANCESTOR', + levelDepth: level, + amount: levelAmount, + reason: `转让转入:第${level}级无上线`, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + }); + continue; + } + + const ancestorAccount = ancestorAccounts.get(ancestor.accountSequence); + const unlockedLevelDepth = ancestorAccount?.unlockedLevelDepth ?? 0; + + if (unlockedLevelDepth >= level) { + // 上线已解锁 → 分配给上线 + await this.createTransferRecord({ + accountSequence: ancestor.accountSequence, + sourceType: 'TRANSFER_IN_TEAM_LEVEL', + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_LEVEL_PER, + levelDepth: level, + amount: levelAmount, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转入L${level}团队算力`, + }); + await this.incrementAccountEffective(ancestor.accountSequence, levelAmount); + await this.incrementLevelPending(ancestor.accountSequence, level, levelAmount); + } else { + // 上线未解锁 → UnallocatedContribution + await this.createUnallocatedContribution({ + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + unallocType: 'LEVEL_OVERFLOW', + wouldBeAccountSequence: ancestor.accountSequence, + levelDepth: level, + amount: levelAmount, + reason: `转让转入:上线${ancestor.accountSequence}未解锁第${level}级`, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + }); + } + } + } else { + // 买方无推荐链,15级全部归总部 + for (let level = 1; level <= 15; level++) { + const levelAmount = totalContribution.times(RATE_LEVEL_PER); + await this.createUnallocatedContribution({ + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + unallocType: 'LEVEL_NO_ANCESTOR', + levelDepth: level, + amount: levelAmount, + reason: `转让转入:第${level}级无上线(买方无推荐关系)`, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + }); + } + } + + // 3. 买方加成分配(每档2.5%,最多3档) + // 买方获得树后 hasAdopted = true + const effectiveHasAdopted = buyerAccount.hasAdopted || true; + const directReferralAdoptedCount = buyerAccount.directReferralAdoptedCount; + + // 计算买方解锁的加成档位 + let unlockedBonusTiers = 0; + if (effectiveHasAdopted) unlockedBonusTiers = 1; + if (directReferralAdoptedCount >= 2) unlockedBonusTiers = 2; + if (directReferralAdoptedCount >= 4) unlockedBonusTiers = 3; + + for (let tier = 1; tier <= 3; tier++) { + const bonusAmount = totalContribution.times(RATE_BONUS_PER); + + if (unlockedBonusTiers >= tier) { + await this.createTransferRecord({ + accountSequence: event.buyerAccountSequence, + sourceType: 'TRANSFER_IN_BONUS', + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + treeCount: event.treeCount, + baseContribution, + distributionRate: RATE_BONUS_PER, + bonusTier: tier, + amount: bonusAmount, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + remark: `转让转入加成T${tier}`, + }); + await this.incrementAccountEffective(event.buyerAccountSequence, bonusAmount); + await this.incrementBonusPending(event.buyerAccountSequence, tier, bonusAmount); + } else { + await this.createUnallocatedContribution({ + sourceAdoptionId, + sourceAccountSequence: event.buyerAccountSequence, + unallocType: `BONUS_TIER_${tier}`, + wouldBeAccountSequence: event.buyerAccountSequence, + amount: bonusAmount, + reason: `转让转入:买方${event.buyerAccountSequence}未解锁第${tier}档加成`, + transferOrderNo: event.transferOrderNo, + effectiveDate: transferDate, + expireDate, + }); + } + } + + // 4. 买方省公司系统账户增加(1%) + const provinceAmount = totalContribution.times(RATE_PROVINCE); + await this.adjustSystemAccount( + 'PROVINCE', + event.selectedProvince, + sourceAdoptionId, + event.buyerAccountSequence, + 'TRANSFER_IN_SYSTEM_PROVINCE', + RATE_PROVINCE, + provinceAmount, // 正数 + transferDate, + expireDate, + event.transferOrderNo, + ); + + // 5. 买方市公司系统账户增加(2%) + const cityAmount = totalContribution.times(RATE_CITY); + await this.adjustSystemAccount( + 'CITY', + event.selectedCity, + sourceAdoptionId, + event.buyerAccountSequence, + 'TRANSFER_IN_SYSTEM_CITY', + RATE_CITY, + cityAmount, // 正数 + transferDate, + expireDate, + event.transferOrderNo, + ); + + // 6. 更新买方解锁状态 + await this.updateBuyerUnlockStatus(event.buyerAccountSequence); + + // 7. 发布算力调整完成确认事件(通知 Saga) + await this.publishTransferAdjustedEvent(event.transferOrderNo); + }); + + this.logger.log( + `[TRANSFER-IN] ✓ Completed: ${event.transferOrderNo}, buyer=${event.buyerAccountSequence}`, + ); + } + + // ============================ + // 私有辅助方法 + // ============================ + + /** + * 直接通过 Prisma 创建算力流水记录(支持负数金额) + */ + private async createTransferRecord(params: { + accountSequence: string; + sourceType: string; + sourceAdoptionId: bigint; + sourceAccountSequence: string; + treeCount: number; + baseContribution: Decimal; + distributionRate: Decimal; + levelDepth?: number; + bonusTier?: number; + amount: Decimal; + transferOrderNo: string; + effectiveDate: Date; + expireDate: Date; + remark?: string; + }): Promise { + await this.unitOfWork.getClient().contributionRecord.create({ + data: { + accountSequence: params.accountSequence, + sourceType: params.sourceType, + sourceAdoptionId: params.sourceAdoptionId, + sourceAccountSequence: params.sourceAccountSequence, + treeCount: params.treeCount, + baseContribution: params.baseContribution, + distributionRate: params.distributionRate, + levelDepth: params.levelDepth ?? null, + bonusTier: params.bonusTier ?? null, + amount: params.amount, + transferOrderNo: params.transferOrderNo, + status: 'EFFECTIVE', + effectiveDate: params.effectiveDate, + expireDate: params.expireDate, + remark: params.remark ?? null, + }, + }); + } + + /** + * 创建未分配算力记录(归总部) + */ + private async createUnallocatedContribution(params: { + sourceAdoptionId: bigint; + sourceAccountSequence: string; + unallocType: string; + wouldBeAccountSequence?: string; + levelDepth?: number; + amount: Decimal; + reason: string; + transferOrderNo: string; + effectiveDate: Date; + expireDate: Date; + }): Promise { + await this.unitOfWork.getClient().unallocatedContribution.create({ + data: { + sourceAdoptionId: params.sourceAdoptionId, + sourceAccountSequence: params.sourceAccountSequence, + unallocType: params.unallocType, + wouldBeAccountSequence: params.wouldBeAccountSequence ?? null, + levelDepth: params.levelDepth ?? null, + amount: params.amount, + reason: params.reason, + transferOrderNo: params.transferOrderNo, + status: 'PENDING', + effectiveDate: params.effectiveDate, + expireDate: params.expireDate, + }, + }); + + // 汇总到总部系统账户 + await this.systemAccountRepository.addContribution( + 'HEADQUARTERS', + null, + new ContributionAmount(params.amount), + ); + } + + /** + * 标记未分配的层级算力失效(转让场景) + */ + private async invalidateUnallocatedForTransfer( + sourceAdoptionId: bigint, + sourceAccountSequence: string, + levelDepth: number, + transferOrderNo: string, + ): Promise { + await this.unitOfWork.getClient().unallocatedContribution.updateMany({ + where: { + sourceAdoptionId, + sourceAccountSequence, + levelDepth, + status: 'PENDING', + unallocType: { in: ['LEVEL_OVERFLOW', 'LEVEL_NO_ANCESTOR'] }, + }, + data: { + status: 'ALLOCATED_TO_HQ', + allocatedAt: new Date(), + }, + }); + } + + /** + * 标记未分配的加成算力失效(转让场景) + */ + private async invalidateUnallocatedBonusForTransfer( + sourceAdoptionId: bigint, + sourceAccountSequence: string, + bonusTier: number, + transferOrderNo: string, + ): Promise { + await this.unitOfWork.getClient().unallocatedContribution.updateMany({ + where: { + sourceAdoptionId, + sourceAccountSequence, + unallocType: `BONUS_TIER_${bonusTier}`, + status: 'PENDING', + }, + data: { + status: 'ALLOCATED_TO_HQ', + allocatedAt: new Date(), + }, + }); + } + + /** + * 调整系统账户(省/市) + */ + private async adjustSystemAccount( + accountType: 'PROVINCE' | 'CITY', + regionCode: string, + sourceAdoptionId: bigint, + sourceAccountSequence: string, + sourceType: string, + distributionRate: Decimal, + amount: Decimal, // 可正可负 + effectiveDate: Date, + expireDate: Date, + transferOrderNo: string, + ): Promise { + // 确保系统账户存在 + await this.systemAccountRepository.ensureSystemAccountsExist(); + + const systemAccount = await this.systemAccountRepository.findByTypeAndRegion( + accountType, + regionCode, + ); + + if (!systemAccount) { + // 动态创建省市系统账户 + await this.systemAccountRepository.addContribution( + accountType, + regionCode, + new ContributionAmount(0), + ); + } + + // 使用 Prisma 直接更新(支持负数 increment) + const sa = await this.unitOfWork.getClient().systemAccount.findFirst({ + where: { accountType, regionCode }, + }); + + if (sa) { + await this.unitOfWork.getClient().systemAccount.update({ + where: { id: sa.id }, + data: { + contributionBalance: { increment: amount }, + }, + }); + + // 创建系统账户明细记录 + await this.unitOfWork.getClient().systemContributionRecord.create({ + data: { + systemAccountId: sa.id, + sourceAdoptionId, + sourceAccountSequence, + sourceType, + distributionRate: distributionRate.toNumber(), + amount, + transferOrderNo, + effectiveDate, + expireDate, + }, + }); + } + } + + // ============================ + // ContributionAccount 原子更新方法 + // ============================ + + private async incrementAccountEffective(accountSequence: string, amount: Decimal): Promise { + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + effectiveContribution: { increment: amount }, + updatedAt: new Date(), + }, + }); + } + + private async decrementAccountEffective(accountSequence: string, amount: Decimal): Promise { + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + effectiveContribution: { decrement: amount }, + updatedAt: new Date(), + }, + }); + } + + private async incrementPersonalContribution(accountSequence: string, amount: Decimal): Promise { + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + personalContribution: { increment: amount }, + updatedAt: new Date(), + }, + }); + } + + private async decrementPersonalContribution(accountSequence: string, amount: Decimal): Promise { + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + personalContribution: { decrement: amount }, + updatedAt: new Date(), + }, + }); + } + + private async incrementLevelPending(accountSequence: string, level: number, amount: Decimal): Promise { + const levelField = `level${level}Pending`; + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + [levelField]: { increment: amount }, + totalLevelPending: { increment: amount }, + totalPending: { increment: amount }, + updatedAt: new Date(), + }, + }); + } + + private async decrementLevelPending(accountSequence: string, level: number, amount: Decimal): Promise { + const levelField = `level${level}Pending`; + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + [levelField]: { decrement: amount }, + totalLevelPending: { decrement: amount }, + totalPending: { decrement: amount }, + updatedAt: new Date(), + }, + }); + } + + private async incrementBonusPending(accountSequence: string, tier: number, amount: Decimal): Promise { + const tierField = `bonusTier${tier}Pending`; + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + [tierField]: { increment: amount }, + totalBonusPending: { increment: amount }, + totalPending: { increment: amount }, + updatedAt: new Date(), + }, + }); + } + + private async decrementBonusPending(accountSequence: string, tier: number, amount: Decimal): Promise { + const tierField = `bonusTier${tier}Pending`; + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence }, + data: { + [tierField]: { decrement: amount }, + totalBonusPending: { decrement: amount }, + totalPending: { decrement: amount }, + updatedAt: new Date(), + }, + }); + } + + // ============================ + // 解锁状态更新 + // ============================ + + /** + * 更新卖方解锁状态 + * 如果卖方不再持有任何树,需要更新 hasAdopted 和相关解锁深度 + */ + private async updateSellerUnlockStatus(sellerAccountSequence: string): Promise { + // 检查卖方是否还有其他已同步的认种记录 + const adoptions = await this.syncedDataRepository.findAdoptionsByAccountSequence( + sellerAccountSequence, + ); + + // 过滤掉 status 不是 MINING_ENABLED 的 + const activeAdoptions = adoptions.filter((a) => a.status === 'MINING_ENABLED'); + + if (activeAdoptions.length === 0) { + // 卖方不再持有任何活跃树 → 重置解锁状态 + const account = await this.contributionAccountRepository.findByAccountSequence( + sellerAccountSequence, + ); + if (account && account.hasAdopted) { + // 注意:不追回历史分配,只更新状态影响未来分配 + await this.unitOfWork.getClient().contributionAccount.update({ + where: { accountSequence: sellerAccountSequence }, + data: { + hasAdopted: false, + unlockedLevelDepth: 0, + // unlockedBonusTiers 的重新计算:T1 取决于 hasAdopted + unlockedBonusTiers: account.directReferralAdoptedCount >= 4 ? 3 : + account.directReferralAdoptedCount >= 2 ? 2 : 0, + updatedAt: new Date(), + }, + }); + + this.logger.log( + `[TRANSFER-OUT] Seller ${sellerAccountSequence} no longer holds trees, reset unlock status`, + ); + } + } + } + + /** + * 更新买方解锁状态 + * 买方获得树后标记 hasAdopted,触发解锁和可能的奖励补发 + */ + private async updateBuyerUnlockStatus(buyerAccountSequence: string): Promise { + const account = await this.contributionAccountRepository.findByAccountSequence( + buyerAccountSequence, + ); + if (!account) return; + + if (!account.hasAdopted) { + // 首次获得树 → 解锁 L1-5 + T1 + account.markAsAdopted(); + await this.contributionAccountRepository.save(account); + + this.logger.log( + `[TRANSFER-IN] Buyer ${buyerAccountSequence} marked as adopted, unlocked L1-5 + T1`, + ); + } + + // 检查买方的推荐人的 directReferralAdoptedCount 是否需要更新 + const buyerReferral = await this.syncedDataRepository.findSyncedReferralByAccountSequence( + buyerAccountSequence, + ); + if (buyerReferral?.referrerAccountSequence) { + const newCount = await this.syncedDataRepository.getDirectReferralAdoptedCount( + buyerReferral.referrerAccountSequence, + ); + + const referrerAccount = await this.contributionAccountRepository.findByAccountSequence( + buyerReferral.referrerAccountSequence, + ); + if (referrerAccount && newCount > referrerAccount.directReferralAdoptedCount) { + const previousCount = referrerAccount.directReferralAdoptedCount; + + // 更新直推认种计数 + for (let i = previousCount; i < newCount; i++) { + referrerAccount.incrementDirectReferralAdoptedCount(); + } + await this.contributionAccountRepository.save(referrerAccount); + + // 触发奖励补发检查 + await this.bonusClaimService.checkAndClaimBonus( + buyerReferral.referrerAccountSequence, + previousCount, + newCount, + ); + + this.logger.log( + `[TRANSFER-IN] Updated referrer ${buyerReferral.referrerAccountSequence} ` + + `directReferralAdoptedCount: ${previousCount} → ${newCount}`, + ); + } + } + } + + /** + * 发布算力调整完成确认事件(通知 Saga) + */ + private async publishTransferAdjustedEvent(transferOrderNo: string): Promise { + await this.outboxRepository.save({ + aggregateType: 'TransferAdjustment', + aggregateId: transferOrderNo, + eventType: 'ContributionTransferAdjusted', + topic: 'contribution.transfer.adjusted', // 显式指定 topic + key: transferOrderNo, + payload: { + transferOrderNo, + consumerService: 'contribution-service', + success: true, + confirmedAt: new Date().toISOString(), + }, + }); + + this.logger.log( + `[TRANSFER] Published contribution.transfer.adjusted for ${transferOrderNo}`, + ); + } +} diff --git a/backend/services/contribution-service/src/domain/aggregates/contribution-account.aggregate.ts b/backend/services/contribution-service/src/domain/aggregates/contribution-account.aggregate.ts index 3cffcc6d..5616bea0 100644 --- a/backend/services/contribution-service/src/domain/aggregates/contribution-account.aggregate.ts +++ b/backend/services/contribution-service/src/domain/aggregates/contribution-account.aggregate.ts @@ -8,6 +8,16 @@ export enum ContributionSourceType { PERSONAL = 'PERSONAL', // 来自自己认种 TEAM_LEVEL = 'TEAM_LEVEL', // 来自团队层级 (1-15级) TEAM_BONUS = 'TEAM_BONUS', // 来自团队加成奖励 (3档) + + // ========== 转让相关(纯新增)========== + // 卖方转出(负数金额) + TRANSFER_OUT_PERSONAL = 'TRANSFER_OUT_PERSONAL', // 卖方个人算力转出 + TRANSFER_OUT_TEAM_LEVEL = 'TRANSFER_OUT_TEAM_LEVEL', // 卖方上线团队层级算力转出 + TRANSFER_OUT_BONUS = 'TRANSFER_OUT_BONUS', // 卖方个人加成算力转出 + // 买方转入(正数金额) + TRANSFER_IN_PERSONAL = 'TRANSFER_IN_PERSONAL', // 买方个人算力转入 + TRANSFER_IN_TEAM_LEVEL = 'TRANSFER_IN_TEAM_LEVEL', // 买方上线团队层级算力转入 + TRANSFER_IN_BONUS = 'TRANSFER_IN_BONUS', // 买方个人加成算力转入 } /** diff --git a/backend/services/deploy.sh b/backend/services/deploy.sh index eb2b5d90..281f93a2 100755 --- a/backend/services/deploy.sh +++ b/backend/services/deploy.sh @@ -390,6 +390,7 @@ migrate() { "admin-service" "presence-service" "blockchain-service" + "transfer-service" ) for svc in "${services[@]}"; do diff --git a/backend/services/docker-compose.yml b/backend/services/docker-compose.yml index 5695ea97..8a2ab25f 100644 --- a/backend/services/docker-compose.yml +++ b/backend/services/docker-compose.yml @@ -20,7 +20,7 @@ services: TZ: Asia/Shanghai POSTGRES_USER: ${POSTGRES_USER:-rwa_user} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rwa_secure_password} - POSTGRES_MULTIPLE_DATABASES: rwa_identity,rwa_wallet,rwa_mpc,rwa_backup,rwa_planting,rwa_referral,rwa_reward,rwa_leaderboard,rwa_reporting,rwa_authorization,rwa_admin,rwa_presence,rwa_blockchain + POSTGRES_MULTIPLE_DATABASES: rwa_identity,rwa_wallet,rwa_mpc,rwa_backup,rwa_planting,rwa_referral,rwa_reward,rwa_leaderboard,rwa_reporting,rwa_authorization,rwa_admin,rwa_presence,rwa_blockchain,rwa_transfer ports: # 安全加固: 仅绑定 127.0.0.1, 禁止公网直连数据库 - "127.0.0.1:5432:5432" @@ -782,6 +782,54 @@ services: networks: - rwa-network + # =========================================================================== + # Transfer Service (树转让服务) + # =========================================================================== + transfer-service: + build: + context: ./transfer-service + dockerfile: Dockerfile + container_name: rwa-transfer-service + ports: + - "3013:3013" + environment: + - TZ=Asia/Shanghai + - NODE_ENV=production + - APP_PORT=3013 + - DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_transfer?schema=public + - JWT_SECRET=${JWT_SECRET} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_DB=17 + - KAFKA_BROKERS=kafka:29092 + - KAFKA_CLIENT_ID=transfer-service + - KAFKA_GROUP_ID=transfer-service-group + - WALLET_SERVICE_URL=http://wallet-service:3001 + - IDENTITY_SERVICE_URL=http://identity-service:3000 + - PLANTING_SERVICE_URL=http://planting-service:3003 + - REFERRAL_SERVICE_URL=http://referral-service:3004 + - CONTRIBUTION_SERVICE_URL=http://contribution-service:3020 + - OUTBOX_POLL_INTERVAL_MS=1000 + - OUTBOX_BATCH_SIZE=100 + - OUTBOX_CONFIRMATION_TIMEOUT_MINUTES=5 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + kafka: + condition: service_started + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3013/api/v1/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 60s + restart: unless-stopped + networks: + - rwa-network + # =========================================================================== # Volumes # =========================================================================== diff --git a/backend/services/planting-service/prisma/schema.prisma b/backend/services/planting-service/prisma/schema.prisma index f3205346..d031dae4 100644 --- a/backend/services/planting-service/prisma/schema.prisma +++ b/backend/services/planting-service/prisma/schema.prisma @@ -20,6 +20,9 @@ model PlantingOrder { treeCount Int @map("tree_count") totalAmount Decimal @map("total_amount") @db.Decimal(20, 8) + // 转让锁定(纯新增,不影响现有逻辑) + transferLockedCount Int @default(0) @map("transfer_locked_count") + // 省市选择 (不可修改) selectedProvince String? @map("selected_province") @db.VarChar(10) selectedCity String? @map("selected_city") @db.VarChar(10) @@ -535,3 +538,47 @@ model PrePlantingRewardEntry { @@index([createdAt]) @@map("pre_planting_reward_entries") } + +// ============================================ +// 转让记录表(纯新增,树转让功能) +// 记录每笔树转让在 planting-service 中的状态 +// LOCKED → TRANSFERRED → ROLLED_BACK +// ============================================ +model PlantingTransferRecord { + id BigInt @id @default(autoincrement()) + transferOrderNo String @unique @map("transfer_order_no") @db.VarChar(50) + + // ========== 原订单信息 ========== + sourceOrderNo String @map("source_order_no") @db.VarChar(50) + sourceOrderId BigInt @map("source_order_id") + + // ========== 卖方 ========== + fromUserId BigInt @map("from_user_id") + fromAccountSequence String @map("from_account_sequence") @db.VarChar(20) + + // ========== 买方(LOCK 阶段为空,EXECUTE 阶段填入)========== + toUserId BigInt? @map("to_user_id") + toAccountSequence String? @map("to_account_sequence") @db.VarChar(20) + + // ========== 转让内容 ========== + treeCount Int @map("tree_count") + selectedProvince String @map("selected_province") @db.VarChar(10) + selectedCity String @map("selected_city") @db.VarChar(10) + contributionPerTree Decimal @map("contribution_per_tree") @db.Decimal(20, 10) + originalAdoptionDate DateTime @map("original_adoption_date") @db.Date + originalExpireDate DateTime @map("original_expire_date") @db.Date + + // ========== 状态 ========== + status String @default("LOCKED") @map("status") @db.VarChar(20) + + lockedAt DateTime? @map("locked_at") + transferredAt DateTime? @map("transferred_at") + rolledBackAt DateTime? @map("rolled_back_at") + createdAt DateTime @default(now()) @map("created_at") + + @@index([sourceOrderNo]) + @@index([fromUserId]) + @@index([toUserId]) + @@index([status]) + @@map("planting_transfer_records") +} diff --git a/backend/services/planting-service/src/api/api.module.ts b/backend/services/planting-service/src/api/api.module.ts index 9a187ffb..1c7b4c0d 100644 --- a/backend/services/planting-service/src/api/api.module.ts +++ b/backend/services/planting-service/src/api/api.module.ts @@ -9,6 +9,8 @@ import { } from './controllers/contract-signing.controller'; // [2026-02-05] 新增:合同管理内部 API import { ContractAdminController } from './controllers/contract-admin.controller'; +// [纯新增] 转让内部 API +import { InternalTransferController } from './controllers/internal-transfer.controller'; import { ApplicationModule } from '../application/application.module'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; @@ -23,6 +25,8 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard'; ContractSigningConfigController, // [2026-02-05] 新增:合同管理内部 API ContractAdminController, + // [纯新增] 转让内部 API + InternalTransferController, ], providers: [JwtAuthGuard], }) diff --git a/backend/services/planting-service/src/api/controllers/internal-transfer.controller.ts b/backend/services/planting-service/src/api/controllers/internal-transfer.controller.ts new file mode 100644 index 00000000..e6e0fb4c --- /dev/null +++ b/backend/services/planting-service/src/api/controllers/internal-transfer.controller.ts @@ -0,0 +1,152 @@ +/** + * 转让内部 API 控制器(纯新增) + * 供 transfer-service 内部调用查询认种订单和持仓数据 + * 不需要 JWT 认证(服务间内部调用) + * + * 回滚方式:删除此文件并从 api.module.ts 中移除引用 + */ + +import { + Controller, + Get, + Param, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; + +@ApiTags('内部接口-转让') +@Controller('internal') +export class InternalTransferController { + private readonly logger = new Logger(InternalTransferController.name); + + constructor(private readonly prisma: PrismaService) {} + + /** + * 查询认种订单详情(含转让可用信息) + * 供 transfer-service 在创建转让订单时验证 + */ + @Get('planting-orders/:orderNo') + @ApiOperation({ summary: '查询认种订单(内部)' }) + async getPlantingOrder( + @Param('orderNo') orderNo: string, + ): Promise<{ + id: string; + orderNo: string; + userId: string; + accountSequence: string; + treeCount: number; + status: string; + selectedProvince: string; + selectedCity: string; + transferLockedCount: number; + transferredCount: number; + availableForTransfer: number; + miningEnabledAt: string | null; + createdAt: string; + }> { + const order = await this.prisma.plantingOrder.findUnique({ + where: { orderNo }, + }); + + if (!order) { + throw new NotFoundException(`认种订单 ${orderNo} 不存在`); + } + + // 计算已完成转让的棵数 + const transferredAgg = + await this.prisma.plantingTransferRecord.aggregate({ + where: { sourceOrderNo: orderNo, status: 'TRANSFERRED' }, + _sum: { treeCount: true }, + }); + const transferredCount = transferredAgg._sum.treeCount || 0; + const availableForTransfer = + order.treeCount - + (order.transferLockedCount || 0) - + transferredCount; + + return { + id: order.id.toString(), + orderNo: order.orderNo, + userId: order.userId.toString(), + accountSequence: order.accountSequence, + treeCount: order.treeCount, + status: order.status, + selectedProvince: order.selectedProvince || '', + selectedCity: order.selectedCity || '', + transferLockedCount: order.transferLockedCount || 0, + transferredCount, + availableForTransfer: Math.max(0, availableForTransfer), + miningEnabledAt: order.miningEnabledAt?.toISOString() || null, + createdAt: order.createdAt.toISOString(), + }; + } + + /** + * 查询用户持仓 + * 供 transfer-service 查询买卖方持仓信息 + */ + @Get('planting-positions/:userId') + @ApiOperation({ summary: '查询用户持仓(内部)' }) + async getUserPosition( + @Param('userId') userId: string, + ): Promise<{ + userId: string; + totalTreeCount: number; + effectiveTreeCount: number; + }> { + const position = await this.prisma.plantingPosition.findUnique({ + where: { userId: BigInt(userId) }, + }); + + if (!position) { + throw new NotFoundException(`用户 ${userId} 持仓不存在`); + } + + return { + userId: position.userId.toString(), + totalTreeCount: position.totalTreeCount, + effectiveTreeCount: position.effectiveTreeCount, + }; + } + + /** + * 查询树的原始算力值和日期信息 + * 供 transfer-service 创建转让订单时获取转让标的信息 + */ + @Get('planting-orders/:orderNo/contribution') + @ApiOperation({ summary: '查询认种订单算力信息(内部)' }) + async getContributionPerTree( + @Param('orderNo') orderNo: string, + ): Promise<{ + contributionPerTree: string; + adoptionDate: string; + expireDate: string; + }> { + const order = await this.prisma.plantingOrder.findUnique({ + where: { orderNo }, + }); + + if (!order) { + throw new NotFoundException(`认种订单 ${orderNo} 不存在`); + } + + // 每棵树的基础算力值 = 总金额 / 棵数 + const contributionPerTree = order.totalAmount.div(order.treeCount); + + // 认种生效日期(优先使用支付时间) + const adoptionDate = + order.paidAt || order.miningEnabledAt || order.createdAt; + + // 到期日期(认种合同期 5 年) + const expireDate = new Date(adoptionDate); + expireDate.setFullYear(expireDate.getFullYear() + 5); + + return { + contributionPerTree: contributionPerTree.toString(), + adoptionDate: adoptionDate.toISOString(), + expireDate: expireDate.toISOString(), + }; + } +} diff --git a/backend/services/planting-service/src/infrastructure/infrastructure.module.ts b/backend/services/planting-service/src/infrastructure/infrastructure.module.ts index a9abe9d2..c169c5bb 100644 --- a/backend/services/planting-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/planting-service/src/infrastructure/infrastructure.module.ts @@ -17,6 +17,10 @@ import { OutboxPublisherService } from './kafka/outbox-publisher.service'; import { EventAckController } from './kafka/event-ack.controller'; import { ContractSigningEventConsumer } from './kafka/contract-signing-event.consumer'; import { KycVerifiedEventConsumer } from './kafka/kyc-verified-event.consumer'; +// [纯新增] 转让事件处理器 +import { TransferLockHandler } from './kafka/transfer-lock.handler'; +import { TransferExecuteHandler } from './kafka/transfer-execute.handler'; +import { TransferRollbackHandler } from './kafka/transfer-rollback.handler'; import { PdfGeneratorService } from './pdf/pdf-generator.service'; import { MinioStorageService } from './storage/minio-storage.service'; import { PLANTING_ORDER_REPOSITORY } from '../domain/repositories/planting-order.repository.interface'; @@ -36,7 +40,15 @@ import { ContractSigningService } from '../application/services/contract-signing }), KafkaModule, ], - controllers: [EventAckController, ContractSigningEventConsumer, KycVerifiedEventConsumer], + controllers: [ + EventAckController, + ContractSigningEventConsumer, + KycVerifiedEventConsumer, + // [纯新增] 转让事件处理器 + TransferLockHandler, + TransferExecuteHandler, + TransferRollbackHandler, + ], providers: [ PrismaService, { diff --git a/backend/services/planting-service/src/infrastructure/kafka/transfer-execute.handler.ts b/backend/services/planting-service/src/infrastructure/kafka/transfer-execute.handler.ts new file mode 100644 index 00000000..d1eca21a --- /dev/null +++ b/backend/services/planting-service/src/infrastructure/kafka/transfer-execute.handler.ts @@ -0,0 +1,241 @@ +/** + * 转让执行处理器(纯新增) + * 消费 transfer.ownership.execute 事件 + * 执行所有权变更:卖方减持 → 买方增持 → 发布 Outbox 事件 + * + * 回滚方式:删除此文件并从 infrastructure.module.ts 中移除引用 + */ + +import { Controller, Logger } from '@nestjs/common'; +import { EventPattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../persistence/prisma/prisma.service'; + +interface TransferExecutePayload { + transferOrderNo: string; + sellerUserId: string; + sellerAccountSequence: string; + buyerUserId: string; + buyerAccountSequence: string; + sourceOrderNo: string; + treeCount: number; + contributionPerTree: string; + selectedProvince: string; + selectedCity: string; + originalAdoptionDate: string; + originalExpireDate: string; +} + +@Controller() +export class TransferExecuteHandler { + private readonly logger = new Logger(TransferExecuteHandler.name); + + constructor(private readonly prisma: PrismaService) {} + + @EventPattern('transfer.ownership.execute') + async handle( + @Payload() message: TransferExecutePayload, + @Ctx() context: KafkaContext, + ): Promise { + const { + transferOrderNo, + sellerUserId, + sellerAccountSequence, + buyerUserId, + buyerAccountSequence, + sourceOrderNo, + treeCount, + contributionPerTree, + selectedProvince, + selectedCity, + originalAdoptionDate, + originalExpireDate, + } = message; + + this.logger.log( + `[TRANSFER-EXECUTE] Received execute request: ${transferOrderNo}, ` + + `seller=${sellerUserId} → buyer=${buyerUserId}, trees=${treeCount}`, + ); + + try { + // 幂等性检查:如果已经转让完成,跳过 + const existing = await this.prisma.plantingTransferRecord.findUnique({ + where: { transferOrderNo }, + }); + + if (existing && existing.status === 'TRANSFERRED') { + this.logger.log( + `[TRANSFER-EXECUTE] Already transferred: ${transferOrderNo}`, + ); + return; + } + + if (!existing || existing.status !== 'LOCKED') { + this.logger.error( + `[TRANSFER-EXECUTE] Invalid state: ${transferOrderNo}, status=${existing?.status}`, + ); + return; + } + + const now = new Date(); + const sellerUserIdBigInt = BigInt(sellerUserId); + const buyerUserIdBigInt = BigInt(buyerUserId); + + // 事务:所有权变更 + Outbox 事件 + await this.prisma.executeTransaction(async (tx) => { + // 1. 更新 PlantingTransferRecord → TRANSFERRED + await tx.plantingTransferRecord.update({ + where: { transferOrderNo }, + data: { + toUserId: buyerUserIdBigInt, + toAccountSequence: buyerAccountSequence, + contributionPerTree: new Prisma.Decimal(contributionPerTree), + status: 'TRANSFERRED', + transferredAt: now, + }, + }); + + // 2. 更新 PlantingOrder.transferLockedCount(锁定已消费) + await tx.plantingOrder.update({ + where: { id: existing.sourceOrderId }, + data: { + transferLockedCount: { decrement: treeCount }, + }, + }); + + // 3. 更新卖方 PlantingPosition(减持) + await tx.plantingPosition.update({ + where: { userId: sellerUserIdBigInt }, + data: { + effectiveTreeCount: { decrement: treeCount }, + totalTreeCount: { decrement: treeCount }, + }, + }); + + // 4. 更新卖方 PositionDistribution(减持) + await tx.positionDistribution.update({ + where: { + userId_provinceCode_cityCode: { + userId: sellerUserIdBigInt, + provinceCode: selectedProvince, + cityCode: selectedCity, + }, + }, + data: { + treeCount: { decrement: treeCount }, + }, + }); + + // 5. Upsert 买方 PlantingPosition(增持) + await tx.plantingPosition.upsert({ + where: { userId: buyerUserIdBigInt }, + create: { + userId: buyerUserIdBigInt, + effectiveTreeCount: treeCount, + totalTreeCount: treeCount, + pendingTreeCount: 0, + }, + update: { + effectiveTreeCount: { increment: treeCount }, + totalTreeCount: { increment: treeCount }, + }, + }); + + // 6. Upsert 买方 PositionDistribution(增持) + await tx.positionDistribution.upsert({ + where: { + userId_provinceCode_cityCode: { + userId: buyerUserIdBigInt, + provinceCode: selectedProvince, + cityCode: selectedCity, + }, + }, + create: { + userId: buyerUserIdBigInt, + provinceCode: selectedProvince, + cityCode: selectedCity, + treeCount: treeCount, + }, + update: { + treeCount: { increment: treeCount }, + }, + }); + + // 7. 创建 3 个 Outbox 事件(B 方案,由 OutboxPublisherService 轮询发布) + const commonPayload = { + transferOrderNo, + sourceOrderNo, + sourceOrderId: existing.sourceOrderId.toString(), // bigint → string,供 contribution-service 查找原始认种记录 + treeCount, + contributionPerTree, + selectedProvince, + selectedCity, + originalAdoptionDate, + originalExpireDate, + }; + + await tx.outboxEvent.createMany({ + data: [ + // 事件 1:通知 transfer-service 所有权变更完成 + { + eventType: 'PlantingTransferCompleted', + topic: 'planting.transfer.completed', + key: transferOrderNo, + payload: { + ...commonPayload, + sellerUserId, + sellerAccountSequence, + buyerUserId, + buyerAccountSequence, + status: 'TRANSFERRED', + transferredAt: now.toISOString(), + } as unknown as Prisma.JsonObject, + aggregateId: transferOrderNo, + aggregateType: 'PlantingTransfer', + status: 'PENDING', + }, + // 事件 2:通知 contribution + referral 卖方减持 + { + eventType: 'PlantingOwnershipRemoved', + topic: 'planting.ownership.removed', + key: sellerUserId, + payload: { + ...commonPayload, + sellerUserId, + sellerAccountSequence, + } as unknown as Prisma.JsonObject, + aggregateId: transferOrderNo, + aggregateType: 'PlantingTransfer', + status: 'PENDING', + }, + // 事件 3:通知 contribution + referral 买方增持 + { + eventType: 'PlantingOwnershipAdded', + topic: 'planting.ownership.added', + key: buyerUserId, + payload: { + ...commonPayload, + buyerUserId, + buyerAccountSequence, + } as unknown as Prisma.JsonObject, + aggregateId: transferOrderNo, + aggregateType: 'PlantingTransfer', + status: 'PENDING', + }, + ], + }); + }); + + this.logger.log( + `[TRANSFER-EXECUTE] ✓ Ownership transferred: ${transferOrderNo}, ` + + `seller=${sellerUserId} → buyer=${buyerUserId}, ${treeCount} trees`, + ); + } catch (error) { + this.logger.error( + `[TRANSFER-EXECUTE] ✗ Failed to execute transfer ${transferOrderNo}:`, + error, + ); + // 不抛出异常 - 消息会被 Kafka 重新投递(通过 transfer-service Saga 重试机制) + } + } +} diff --git a/backend/services/planting-service/src/infrastructure/kafka/transfer-lock.handler.ts b/backend/services/planting-service/src/infrastructure/kafka/transfer-lock.handler.ts new file mode 100644 index 00000000..081aec37 --- /dev/null +++ b/backend/services/planting-service/src/infrastructure/kafka/transfer-lock.handler.ts @@ -0,0 +1,183 @@ +/** + * 转让锁定处理器(纯新增) + * 消费 transfer.trees.lock 事件 + * 锁定卖方指定订单的树,防止重复转让 + * + * 回滚方式:删除此文件并从 infrastructure.module.ts 中移除引用 + */ + +import { Controller, Logger, Inject } from '@nestjs/common'; +import { EventPattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; +import { ClientKafka } from '@nestjs/microservices'; +import { PrismaService } from '../persistence/prisma/prisma.service'; + +interface TransferLockPayload { + transferOrderNo: string; + sellerUserId: string; + sourceOrderNo: string; + treeCount: number; + sellerAccountSequence: string; +} + +@Controller() +export class TransferLockHandler { + private readonly logger = new Logger(TransferLockHandler.name); + + constructor( + private readonly prisma: PrismaService, + @Inject('KAFKA_SERVICE') + private readonly kafkaClient: ClientKafka, + ) {} + + @EventPattern('transfer.trees.lock') + async handle( + @Payload() message: TransferLockPayload, + @Ctx() context: KafkaContext, + ): Promise { + const { + transferOrderNo, + sellerUserId, + sourceOrderNo, + treeCount, + sellerAccountSequence, + } = message; + + this.logger.log( + `[TRANSFER-LOCK] Received lock request: ${transferOrderNo}, orderNo=${sourceOrderNo}, trees=${treeCount}`, + ); + + try { + // 幂等性检查:如果已经锁定,直接返回成功 + const existing = await this.prisma.plantingTransferRecord.findUnique({ + where: { transferOrderNo }, + }); + + if (existing) { + this.logger.log( + `[TRANSFER-LOCK] Already locked: ${transferOrderNo}, status=${existing.status}`, + ); + this.publishAck(transferOrderNo, true); + return; + } + + // 验证订单存在且属于卖方 + const order = await this.prisma.plantingOrder.findUnique({ + where: { orderNo: sourceOrderNo }, + }); + + if (!order) { + this.logger.error(`[TRANSFER-LOCK] Order not found: ${sourceOrderNo}`); + this.publishAck(transferOrderNo, false, '认种订单不存在'); + return; + } + + if (order.userId !== BigInt(sellerUserId)) { + this.logger.error( + `[TRANSFER-LOCK] Order ${sourceOrderNo} does not belong to seller ${sellerUserId}`, + ); + this.publishAck(transferOrderNo, false, '订单不属于卖方'); + return; + } + + if (order.status !== 'MINING_ENABLED') { + this.logger.error( + `[TRANSFER-LOCK] Order ${sourceOrderNo} status is ${order.status}, not MINING_ENABLED`, + ); + this.publishAck(transferOrderNo, false, '订单状态不允许转让'); + return; + } + + // 计算可转让棵数 + const transferredAgg = await this.prisma.plantingTransferRecord.aggregate({ + where: { sourceOrderNo, status: 'TRANSFERRED' }, + _sum: { treeCount: true }, + }); + const transferredCount = transferredAgg._sum.treeCount || 0; + const available = + order.treeCount - (order.transferLockedCount || 0) - transferredCount; + + if (available < treeCount) { + this.logger.error( + `[TRANSFER-LOCK] Insufficient trees: available=${available}, requested=${treeCount}`, + ); + this.publishAck( + transferOrderNo, + false, + `可转让棵数不足,可用${available}棵`, + ); + return; + } + + // 计算算力值和日期 + const contributionPerTree = order.totalAmount.div(order.treeCount); + const adoptionDate = order.paidAt || order.miningEnabledAt || order.createdAt; + const expireDate = new Date(adoptionDate); + expireDate.setFullYear(expireDate.getFullYear() + 5); + + // 事务:创建锁定记录 + 更新锁定计数 + await this.prisma.executeTransaction(async (tx) => { + await tx.plantingTransferRecord.create({ + data: { + transferOrderNo, + sourceOrderNo, + sourceOrderId: order.id, + fromUserId: BigInt(sellerUserId), + fromAccountSequence: sellerAccountSequence, + treeCount, + selectedProvince: order.selectedProvince || '', + selectedCity: order.selectedCity || '', + contributionPerTree, + originalAdoptionDate: adoptionDate, + originalExpireDate: expireDate, + status: 'LOCKED', + lockedAt: new Date(), + }, + }); + + await tx.plantingOrder.update({ + where: { id: order.id }, + data: { + transferLockedCount: { increment: treeCount }, + }, + }); + }); + + this.logger.log( + `[TRANSFER-LOCK] ✓ Trees locked: ${transferOrderNo}, ${treeCount} trees from order ${sourceOrderNo}`, + ); + + this.publishAck(transferOrderNo, true); + } catch (error) { + this.logger.error( + `[TRANSFER-LOCK] ✗ Failed to lock trees for ${transferOrderNo}:`, + error, + ); + this.publishAck( + transferOrderNo, + false, + `锁定失败: ${(error as Error).message}`, + ); + } + } + + private publishAck( + transferOrderNo: string, + success: boolean, + error?: string, + ): void { + const message = { + key: transferOrderNo, + value: JSON.stringify({ + transferOrderNo, + success, + error, + confirmedAt: new Date().toISOString(), + }), + }; + + this.kafkaClient.emit('transfer.trees.lock.ack', message); + this.logger.log( + `[TRANSFER-LOCK] ACK sent: ${transferOrderNo}, success=${success}`, + ); + } +} diff --git a/backend/services/planting-service/src/infrastructure/kafka/transfer-rollback.handler.ts b/backend/services/planting-service/src/infrastructure/kafka/transfer-rollback.handler.ts new file mode 100644 index 00000000..3c1e26f7 --- /dev/null +++ b/backend/services/planting-service/src/infrastructure/kafka/transfer-rollback.handler.ts @@ -0,0 +1,131 @@ +/** + * 转让回滚处理器(纯新增) + * 消费 transfer.trees.unlock 事件 + * 解锁卖方的树,恢复可转让状态 + * + * 回滚方式:删除此文件并从 infrastructure.module.ts 中移除引用 + */ + +import { Controller, Logger, Inject } from '@nestjs/common'; +import { EventPattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; +import { ClientKafka } from '@nestjs/microservices'; +import { PrismaService } from '../persistence/prisma/prisma.service'; + +interface TransferUnlockPayload { + transferOrderNo: string; + sourceOrderNo: string; + treeCount: number; +} + +@Controller() +export class TransferRollbackHandler { + private readonly logger = new Logger(TransferRollbackHandler.name); + + constructor( + private readonly prisma: PrismaService, + @Inject('KAFKA_SERVICE') + private readonly kafkaClient: ClientKafka, + ) {} + + @EventPattern('transfer.trees.unlock') + async handle( + @Payload() message: TransferUnlockPayload, + @Ctx() context: KafkaContext, + ): Promise { + const { transferOrderNo, sourceOrderNo, treeCount } = message; + + this.logger.log( + `[TRANSFER-ROLLBACK] Received unlock request: ${transferOrderNo}, orderNo=${sourceOrderNo}`, + ); + + try { + // 幂等性检查 + const existing = await this.prisma.plantingTransferRecord.findUnique({ + where: { transferOrderNo }, + }); + + if (!existing) { + this.logger.warn( + `[TRANSFER-ROLLBACK] Record not found: ${transferOrderNo}, sending success ACK`, + ); + this.publishAck(transferOrderNo, true); + return; + } + + if (existing.status === 'ROLLED_BACK') { + this.logger.log( + `[TRANSFER-ROLLBACK] Already rolled back: ${transferOrderNo}`, + ); + this.publishAck(transferOrderNo, true); + return; + } + + if (existing.status !== 'LOCKED') { + this.logger.error( + `[TRANSFER-ROLLBACK] Cannot rollback: ${transferOrderNo}, status=${existing.status}`, + ); + this.publishAck( + transferOrderNo, + false, + `状态不允许回滚: ${existing.status}`, + ); + return; + } + + // 事务:更新记录状态 + 恢复锁定计数 + await this.prisma.executeTransaction(async (tx) => { + await tx.plantingTransferRecord.update({ + where: { transferOrderNo }, + data: { + status: 'ROLLED_BACK', + rolledBackAt: new Date(), + }, + }); + + await tx.plantingOrder.update({ + where: { id: existing.sourceOrderId }, + data: { + transferLockedCount: { decrement: existing.treeCount }, + }, + }); + }); + + this.logger.log( + `[TRANSFER-ROLLBACK] ✓ Trees unlocked: ${transferOrderNo}, ${existing.treeCount} trees`, + ); + + this.publishAck(transferOrderNo, true); + } catch (error) { + this.logger.error( + `[TRANSFER-ROLLBACK] ✗ Failed to rollback ${transferOrderNo}:`, + error, + ); + this.publishAck( + transferOrderNo, + false, + `回滚失败: ${(error as Error).message}`, + ); + } + } + + private publishAck( + transferOrderNo: string, + success: boolean, + error?: string, + ): void { + const message = { + key: transferOrderNo, + value: JSON.stringify({ + transferOrderNo, + success, + error, + confirmedAt: new Date().toISOString(), + }), + }; + + this.kafkaClient.emit('transfer.trees.unlock.ack', message); + this.logger.log( + `[TRANSFER-ROLLBACK] ACK sent: ${transferOrderNo}, success=${success}`, + ); + } +} diff --git a/backend/services/planting-service/src/main.ts b/backend/services/planting-service/src/main.ts index 98fe936a..22daa2a2 100644 --- a/backend/services/planting-service/src/main.ts +++ b/backend/services/planting-service/src/main.ts @@ -90,9 +90,23 @@ async function bootstrap() { }, }); + // 微服务 4: 用于监听 transfer-service 的转让事件(纯新增) + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: 'planting-service-transfer', + brokers: kafkaBrokers, + }, + consumer: { + groupId: `${kafkaGroupId}-transfer`, + }, + }, + }); + // 启动所有 Kafka 微服务 await app.startAllMicroservices(); - logger.log('Kafka microservices started (ACK + Contract Signing + Identity Events)'); + logger.log('Kafka microservices started (ACK + Contract Signing + Identity Events + Transfer)'); const port = process.env.APP_PORT || 3003; await app.listen(port); diff --git a/backend/services/referral-service/src/application/event-handlers/index.ts b/backend/services/referral-service/src/application/event-handlers/index.ts index 6078d048..aca5eb45 100644 --- a/backend/services/referral-service/src/application/event-handlers/index.ts +++ b/backend/services/referral-service/src/application/event-handlers/index.ts @@ -1,3 +1,5 @@ export * from './user-registered.handler'; export * from './planting-created.handler'; export * from './contract-signing.handler'; +// [纯新增] 转让事件处理器 +export * from './planting-transferred.handler'; diff --git a/backend/services/referral-service/src/application/event-handlers/planting-transferred.handler.ts b/backend/services/referral-service/src/application/event-handlers/planting-transferred.handler.ts new file mode 100644 index 00000000..5f52d77f --- /dev/null +++ b/backend/services/referral-service/src/application/event-handlers/planting-transferred.handler.ts @@ -0,0 +1,316 @@ +/** + * 转让事件处理器(纯新增) + * 消费 planting.ownership.removed + planting.ownership.added 事件 + * 更新卖方/买方及其所有上级的团队统计 + * + * 回滚方式:删除此文件并从 event-handlers/index.ts 和 application.module.ts 中移除引用 + */ + +import { Injectable, Inject, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IReferralRelationshipRepository, + REFERRAL_RELATIONSHIP_REPOSITORY, + ITeamStatisticsRepository, + TEAM_STATISTICS_REPOSITORY, +} from '../../domain'; +import { KafkaService } from '../../infrastructure/messaging/kafka.service'; +import { EventAckPublisher } from '../../infrastructure/kafka/event-ack.publisher'; +import { PrismaService } from '../../infrastructure/database/prisma.service'; + +interface PlantingOwnershipRemovedEvent { + transferOrderNo: string; + sourceOrderNo: string; + sellerUserId: string; + sellerAccountSequence: string; + treeCount: number; + contributionPerTree: string; + selectedProvince: string; + selectedCity: string; + originalAdoptionDate: string; + originalExpireDate: string; + _outbox?: { + id: string; + aggregateId: string; + eventType: string; + ackTopic: string; + }; +} + +interface PlantingOwnershipAddedEvent { + transferOrderNo: string; + sourceOrderNo: string; + buyerUserId: string; + buyerAccountSequence: string; + treeCount: number; + contributionPerTree: string; + selectedProvince: string; + selectedCity: string; + originalAdoptionDate: string; + originalExpireDate: string; + _outbox?: { + id: string; + aggregateId: string; + eventType: string; + ackTopic: string; + }; +} + +@Injectable() +export class PlantingTransferredHandler implements OnModuleInit { + private readonly logger = new Logger(PlantingTransferredHandler.name); + + constructor( + private readonly kafkaService: KafkaService, + private readonly eventAckPublisher: EventAckPublisher, + private readonly prisma: PrismaService, + @Inject(REFERRAL_RELATIONSHIP_REPOSITORY) + private readonly referralRepo: IReferralRelationshipRepository, + @Inject(TEAM_STATISTICS_REPOSITORY) + private readonly teamStatsRepo: ITeamStatisticsRepository, + ) {} + + async onModuleInit() { + // 订阅卖方减持事件 + await this.kafkaService.subscribe( + 'referral-service-transfer-removed', + ['planting.ownership.removed'], + this.handleOwnershipRemoved.bind(this), + ); + + // 订阅买方增持事件 + await this.kafkaService.subscribe( + 'referral-service-transfer-added', + ['planting.ownership.added'], + this.handleOwnershipAdded.bind(this), + ); + + this.logger.log('Subscribed to planting.ownership.removed + planting.ownership.added'); + } + + /** + * 处理卖方减持事件 + * 1. 更新卖方个人认种统计(selfPlantingCount -= treeCount) + * 2. 更新卖方所有上级的团队认种统计(负数 delta,重新扫描大区) + */ + private async handleOwnershipRemoved( + topic: string, + message: Record, + ): Promise { + const event = message as unknown as PlantingOwnershipRemovedEvent; + const outboxInfo = event._outbox; + const eventId = outboxInfo?.aggregateId || event.transferOrderNo; + + this.logger.log( + `[TRANSFER-REMOVED] Processing: transferOrderNo=${event.transferOrderNo}, ` + + `seller=${event.sellerAccountSequence}, trees=${event.treeCount}`, + ); + + try { + // 幂等性检查 + const processedEventId = `removed:${eventId}`; + const existing = await this.prisma.processedEvent.findUnique({ + where: { eventId: processedEventId }, + }); + if (existing) { + this.logger.log(`[TRANSFER-REMOVED] Already processed: ${processedEventId}`); + if (outboxInfo) { + await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType); + } + return; + } + + // 1. 查找卖方推荐关系 + const relationship = await this.referralRepo.findByAccountSequence( + event.sellerAccountSequence, + ); + if (!relationship) { + this.logger.warn( + `[TRANSFER-REMOVED] No referral relationship for ${event.sellerAccountSequence}`, + ); + await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipRemoved', outboxInfo, eventId); + return; + } + + const userId = relationship.userId; + + // 2. 更新卖方个人认种统计 + const sellerStats = await this.teamStatsRepo.findByUserId(userId); + if (sellerStats) { + sellerStats.removePersonalPlanting( + event.treeCount, + event.selectedProvince, + event.selectedCity, + ); + await this.teamStatsRepo.save(sellerStats); + sellerStats.clearDomainEvents(); + } + + // 3. 更新卖方所有上级的团队统计(负数 delta) + const ancestors = relationship.getAllAncestorIds(); + if (ancestors.length > 0) { + const updates = ancestors.map((ancestorId, i) => ({ + userId: ancestorId, + countDelta: -event.treeCount, // 关键:负数 + provinceCode: event.selectedProvince, + cityCode: event.selectedCity, + fromDirectReferralId: i === 0 ? userId : ancestors[0], + })); + + await this.teamStatsRepo.batchUpdateTeamCountsForTransfer(updates); + } + + // 4. 记录已处理 + 发送 ACK + await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipRemoved', outboxInfo, eventId); + + this.logger.log( + `[TRANSFER-REMOVED] ✓ Processed: seller=${event.sellerAccountSequence}, ` + + `trees=${event.treeCount}, ancestors=${ancestors.length}`, + ); + } catch (error) { + this.logger.error( + `[TRANSFER-REMOVED] ✗ Failed for ${event.transferOrderNo}:`, + error, + ); + if (outboxInfo) { + const msg = error instanceof Error ? error.message : String(error); + await this.eventAckPublisher.sendFailure(eventId, outboxInfo.eventType, msg); + } + throw error; + } + } + + /** + * 处理买方增持事件 + * 1. 更新买方个人认种统计(selfPlantingCount += treeCount) + * 2. 更新买方所有上级的团队认种统计(正数 delta,复用现有方法) + * 3. 两方处理完后发布确认事件通知 Saga + */ + private async handleOwnershipAdded( + topic: string, + message: Record, + ): Promise { + const event = message as unknown as PlantingOwnershipAddedEvent; + const outboxInfo = event._outbox; + const eventId = outboxInfo?.aggregateId || event.transferOrderNo; + + this.logger.log( + `[TRANSFER-ADDED] Processing: transferOrderNo=${event.transferOrderNo}, ` + + `buyer=${event.buyerAccountSequence}, trees=${event.treeCount}`, + ); + + try { + // 幂等性检查 + const processedEventId = `added:${eventId}`; + const existing = await this.prisma.processedEvent.findUnique({ + where: { eventId: processedEventId }, + }); + if (existing) { + this.logger.log(`[TRANSFER-ADDED] Already processed: ${processedEventId}`); + if (outboxInfo) { + await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType); + } + return; + } + + // 1. 查找买方推荐关系 + const relationship = await this.referralRepo.findByAccountSequence( + event.buyerAccountSequence, + ); + if (!relationship) { + this.logger.warn( + `[TRANSFER-ADDED] No referral relationship for ${event.buyerAccountSequence}`, + ); + await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipAdded', outboxInfo, eventId); + return; + } + + const userId = relationship.userId; + + // 2. 更新买方个人认种统计 + const buyerStats = await this.teamStatsRepo.findByUserId(userId); + if (buyerStats) { + buyerStats.addPersonalPlanting( + event.treeCount, + event.selectedProvince, + event.selectedCity, + ); + await this.teamStatsRepo.save(buyerStats); + buyerStats.clearDomainEvents(); + } + + // 3. 更新买方所有上级的团队统计(正数 delta,复用现有方法) + const ancestors = relationship.getAllAncestorIds(); + if (ancestors.length > 0) { + const updates = ancestors.map((ancestorId, i) => ({ + userId: ancestorId, + countDelta: event.treeCount, // 正数 + provinceCode: event.selectedProvince, + cityCode: event.selectedCity, + fromDirectReferralId: i === 0 ? userId : ancestors[0], + })); + + await this.teamStatsRepo.batchUpdateTeamCounts(updates); + } + + // 4. 记录已处理 + 发送 ACK + await this.markProcessedAndAck(processedEventId, 'PlantingOwnershipAdded', outboxInfo, eventId); + + // 5. 两方都处理完后,发布确认事件通知 transfer-service Saga 推进 + await this.publishTransferStatsUpdated(event.transferOrderNo); + + this.logger.log( + `[TRANSFER-ADDED] ✓ Processed: buyer=${event.buyerAccountSequence}, ` + + `trees=${event.treeCount}, ancestors=${ancestors.length}`, + ); + } catch (error) { + this.logger.error( + `[TRANSFER-ADDED] ✗ Failed for ${event.transferOrderNo}:`, + error, + ); + if (outboxInfo) { + const msg = error instanceof Error ? error.message : String(error); + await this.eventAckPublisher.sendFailure(eventId, outboxInfo.eventType, msg); + } + throw error; + } + } + + /** + * 发布团队统计更新完成确认(通知 Saga) + */ + private async publishTransferStatsUpdated(transferOrderNo: string): Promise { + await this.kafkaService.publish({ + topic: 'referral.transfer.stats-updated', + key: transferOrderNo, + value: { + transferOrderNo, + consumerService: 'referral-service', + success: true, + confirmedAt: new Date().toISOString(), + }, + }); + + this.logger.log( + `[TRANSFER] Published referral.transfer.stats-updated for ${transferOrderNo}`, + ); + } + + /** + * 标记事件已处理并发送 ACK + */ + private async markProcessedAndAck( + processedEventId: string, + eventType: string, + outboxInfo: PlantingOwnershipRemovedEvent['_outbox'], + eventId: string, + ): Promise { + await this.prisma.processedEvent.create({ + data: { eventId: processedEventId, eventType }, + }); + + if (outboxInfo) { + await this.eventAckPublisher.sendSuccess(eventId, outboxInfo.eventType); + } + } +} diff --git a/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts b/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts index a67866f6..61aea116 100644 --- a/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts +++ b/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts @@ -224,6 +224,33 @@ export class TeamStatistics { ); } + /** + * 减少个人认种量 + * [纯新增] 用于转让卖方减持,与 addPersonalPlanting 对称 + * 注意:个人认种不计入 teamPlantingCount,所以只更新个人统计和省市分布 + */ + removePersonalPlanting(count: number, provinceCode: string, cityCode: string): void { + this._personalPlantingCount = Math.max(0, this._personalPlantingCount - count); + this._provinceCityDistribution = this._provinceCityDistribution.subtract( + provinceCode, + cityCode, + count, + ); + this._lastCalculatedAt = new Date(); + this._updatedAt = new Date(); + + this._domainEvents.push( + new TeamStatisticsUpdatedEvent( + this._userId.value, + this._accountSequence, + this._teamPlantingCount, + this._directReferralCount, + this._leaderboardScore.score, + 'transfer_removed', + ), + ); + } + /** * 重新计算龙虎榜分值 */ diff --git a/backend/services/referral-service/src/domain/events/team-statistics-updated.event.ts b/backend/services/referral-service/src/domain/events/team-statistics-updated.event.ts index 9178464a..1ad908f7 100644 --- a/backend/services/referral-service/src/domain/events/team-statistics-updated.event.ts +++ b/backend/services/referral-service/src/domain/events/team-statistics-updated.event.ts @@ -10,7 +10,7 @@ export class TeamStatisticsUpdatedEvent extends DomainEvent { public readonly totalTeamCount: number, public readonly directReferralCount: number, public readonly leaderboardScore: number, - public readonly updateReason: 'planting_added' | 'planting_removed' | 'member_joined' | 'recalculation', + public readonly updateReason: 'planting_added' | 'planting_removed' | 'member_joined' | 'recalculation' | 'transfer_removed' | 'transfer_added', ) { super(); } diff --git a/backend/services/referral-service/src/domain/repositories/team-statistics.repository.interface.ts b/backend/services/referral-service/src/domain/repositories/team-statistics.repository.interface.ts index e696cb78..a48086b7 100644 --- a/backend/services/referral-service/src/domain/repositories/team-statistics.repository.interface.ts +++ b/backend/services/referral-service/src/domain/repositories/team-statistics.repository.interface.ts @@ -55,6 +55,20 @@ export interface ITeamStatisticsRepository { }>, ): Promise; + /** + * [纯新增] 批量更新团队统计(转让场景,支持负数 delta) + * 与 batchUpdateTeamCounts 区别:负数 delta 时重新扫描所有直推找最大支线 + */ + batchUpdateTeamCountsForTransfer( + updates: Array<{ + userId: bigint; + countDelta: number; + provinceCode: string; + cityCode: string; + fromDirectReferralId?: bigint; + }>, + ): Promise; + /** * 创建初始团队统计记录 */ diff --git a/backend/services/referral-service/src/domain/value-objects/province-city-distribution.vo.ts b/backend/services/referral-service/src/domain/value-objects/province-city-distribution.vo.ts index 73f6b481..dde5280b 100644 --- a/backend/services/referral-service/src/domain/value-objects/province-city-distribution.vo.ts +++ b/backend/services/referral-service/src/domain/value-objects/province-city-distribution.vo.ts @@ -48,6 +48,25 @@ export class ProvinceCityDistribution { return new ProvinceCityDistribution(newDist); } + /** + * 减少认种记录 (返回新实例,不可变) + * [纯新增] 用于转让卖方减持 + */ + subtract(provinceCode: string, cityCode: string, count: number): ProvinceCityDistribution { + const newDist = new Map(this.distribution); + + if (!newDist.has(provinceCode)) { + return new ProvinceCityDistribution(newDist); + } + + const cityMap = new Map(newDist.get(provinceCode)!); + const currentCount = cityMap.get(cityCode) ?? 0; + cityMap.set(cityCode, Math.max(0, currentCount - count)); + newDist.set(provinceCode, cityMap); + + return new ProvinceCityDistribution(newDist); + } + /** * 获取某省的总认种量 */ diff --git a/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts b/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts index 9085173d..713de6e9 100644 --- a/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts +++ b/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts @@ -254,6 +254,99 @@ export class TeamStatisticsRepository implements ITeamStatisticsRepository { }); } + /** + * [纯新增] 批量更新团队统计(转让场景) + * 与 batchUpdateTeamCounts 的关键区别: + * 负数 delta 时重新扫描所有直推的 teamPlantingCount 找最大支线 + * 因为减少后原来的"大区"可能不再是最大的 + */ + async batchUpdateTeamCountsForTransfer( + updates: Array<{ + userId: bigint; + countDelta: number; + provinceCode: string; + cityCode: string; + fromDirectReferralId?: bigint; + }>, + ): Promise { + await this.prisma.$transaction(async (tx) => { + for (const update of updates) { + const current = await tx.teamStatistics.findUnique({ + where: { userId: update.userId }, + }); + + if (!current) continue; + + // 更新省市分布 + const distribution = (current.provinceCityDistribution as Record< + string, + Record + >) ?? {}; + if (!distribution[update.provinceCode]) { + distribution[update.provinceCode] = {}; + } + const currentCityCount = distribution[update.provinceCode][update.cityCode] ?? 0; + distribution[update.provinceCode][update.cityCode] = Math.max( + 0, + currentCityCount + update.countDelta, + ); + + // 更新直推团队统计 + if (update.fromDirectReferralId) { + await tx.directReferral.upsert({ + where: { + uk_referrer_referral: { + referrerId: update.userId, + referralId: update.fromDirectReferralId, + }, + }, + update: { + teamPlantingCount: { increment: update.countDelta }, + }, + create: { + referrerId: update.userId, + referralId: update.fromDirectReferralId, + referralSequence: update.fromDirectReferralId.toString(), + teamPlantingCount: Math.max(0, update.countDelta), + }, + }); + } + + // 关键差异:负数 delta 时重新扫描所有直推找最大支线 + const allDirectReferrals = await tx.directReferral.findMany({ + where: { referrerId: update.userId }, + select: { teamPlantingCount: true }, + }); + const newMaxDirectTeamCount = + allDirectReferrals.length > 0 + ? Math.max(0, ...allDirectReferrals.map((r) => r.teamPlantingCount)) + : 0; + + // 计算新总量和龙虎榜分值 + const newTotalTeamPlantingCount = Math.max( + 0, + current.totalTeamPlantingCount + update.countDelta, + ); + const newEffectiveScore = Math.max( + 0, + newTotalTeamPlantingCount - newMaxDirectTeamCount, + ); + + await tx.teamStatistics.update({ + where: { userId: update.userId }, + data: { + totalTeamPlantingCount: newTotalTeamPlantingCount, + effectivePlantingCountForRanking: newEffectiveScore, + maxSingleTeamPlantingCount: newMaxDirectTeamCount, + provinceCityDistribution: distribution, + lastCalcAt: new Date(), + updatedAt: new Date(), + }, + }); + } + }); + } + async create(userId: bigint, accountSequence: string): Promise { const created = await this.prisma.teamStatistics.create({ data: { diff --git a/backend/services/referral-service/src/modules/application.module.ts b/backend/services/referral-service/src/modules/application.module.ts index 8d2cd9fc..73a91a26 100644 --- a/backend/services/referral-service/src/modules/application.module.ts +++ b/backend/services/referral-service/src/modules/application.module.ts @@ -7,6 +7,8 @@ import { UserRegisteredHandler, PlantingCreatedHandler, ContractSigningHandler, + // [纯新增] 转让事件处理器 + PlantingTransferredHandler, } from '../application'; @Module({ @@ -17,6 +19,8 @@ import { UserRegisteredHandler, PlantingCreatedHandler, ContractSigningHandler, + // [纯新增] 转让事件处理器 + PlantingTransferredHandler, ], exports: [ReferralService, TeamStatisticsService], }) diff --git a/backend/services/scripts/init-databases.sh b/backend/services/scripts/init-databases.sh index 3280b806..dd4bcc00 100644 --- a/backend/services/scripts/init-databases.sh +++ b/backend/services/scripts/init-databases.sh @@ -12,7 +12,7 @@ EOSQL } # Create all required databases -for db in rwa_identity rwa_wallet rwa_mpc rwa_backup rwa_planting rwa_referral rwa_reward rwa_leaderboard rwa_reporting rwa_authorization rwa_admin rwa_presence rwa_blockchain; do +for db in rwa_identity rwa_wallet rwa_mpc rwa_backup rwa_planting rwa_referral rwa_reward rwa_leaderboard rwa_reporting rwa_authorization rwa_admin rwa_presence rwa_blockchain rwa_transfer; do create_database "$db" done diff --git a/backend/services/transfer-service/.env.development b/backend/services/transfer-service/.env.development new file mode 100644 index 00000000..17373e5c --- /dev/null +++ b/backend/services/transfer-service/.env.development @@ -0,0 +1,34 @@ +# Database +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwa_transfer?schema=public" + +# App +NODE_ENV=development +APP_PORT=3013 + +# JWT +JWT_SECRET="transfer-service-dev-jwt-secret" + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=17 + +# External Services +WALLET_SERVICE_URL=http://localhost:3001 +IDENTITY_SERVICE_URL=http://localhost:3000 +PLANTING_SERVICE_URL=http://localhost:3003 +REFERRAL_SERVICE_URL=http://localhost:3004 +CONTRIBUTION_SERVICE_URL=http://localhost:3020 + +# Kafka Configuration +KAFKA_BROKERS=localhost:9092 +KAFKA_CLIENT_ID=transfer-service +KAFKA_GROUP_ID=transfer-service-group + +# Outbox Configuration +OUTBOX_POLL_INTERVAL_MS=1000 +OUTBOX_BATCH_SIZE=100 +OUTBOX_CONFIRMATION_TIMEOUT_MINUTES=5 +OUTBOX_CLEANUP_INTERVAL_MS=3600000 +OUTBOX_RETENTION_DAYS=7 diff --git a/backend/services/transfer-service/.gitignore b/backend/services/transfer-service/.gitignore new file mode 100644 index 00000000..f1522176 --- /dev/null +++ b/backend/services/transfer-service/.gitignore @@ -0,0 +1,30 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* + +# OS +.DS_Store + +# Tests +/coverage + +# IDE +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Environment +.env +.env.production + +# Prisma +prisma/migrations/migration_lock.toml diff --git a/backend/services/transfer-service/Dockerfile b/backend/services/transfer-service/Dockerfile new file mode 100644 index 00000000..b43a2c6a --- /dev/null +++ b/backend/services/transfer-service/Dockerfile @@ -0,0 +1,62 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy prisma schema and generate client (dummy DATABASE_URL for build time only) +COPY prisma ./prisma/ +RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate + +# Copy source code +COPY . . + +# Build +RUN npm run build + +# Production stage - use Debian slim for OpenSSL compatibility +FROM node:20-slim AS production + +WORKDIR /app + +# Install OpenSSL and curl for health checks +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy package files +COPY package*.json ./ + +# Install production dependencies +RUN npm ci --only=production + +# Copy prisma schema and generate client (dummy DATABASE_URL for build time only) +COPY prisma ./prisma/ +RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate + +# Copy built application +COPY --from=builder /app/dist ./dist + +# Create startup script that runs migrations before starting the app +RUN echo '#!/bin/sh\n\ +set -e\n\ +echo "Running database migrations..."\n\ +npx prisma migrate deploy\n\ +echo "Starting application..."\n\ +exec node dist/main.js\n' > /app/start.sh && chmod +x /app/start.sh + +# Expose port +EXPOSE 3013 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:3013/api/v1/health || exit 1 + +# Start service with migration +CMD ["/app/start.sh"] diff --git a/backend/services/transfer-service/nest-cli.json b/backend/services/transfer-service/nest-cli.json new file mode 100644 index 00000000..f9aa683b --- /dev/null +++ b/backend/services/transfer-service/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/services/transfer-service/package-lock.json b/backend/services/transfer-service/package-lock.json new file mode 100644 index 00000000..23c1806b --- /dev/null +++ b/backend/services/transfer-service/package-lock.json @@ -0,0 +1,10284 @@ +{ + "name": "transfer-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "transfer-service", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/axios": "^3.0.1", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.0.0", + "@nestjs/swagger": "^7.1.17", + "@prisma/client": "^5.7.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "jsonwebtoken": "^9.0.0", + "kafkajs": "^2.2.4", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/jsonwebtoken": "^9.0.0", + "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.0", + "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "prisma": "^5.7.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@nestjs/axios": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz", + "integrity": "sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@nestjs/jwt/node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/@nestjs/jwt/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@nestjs/jwt/node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/microservices": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.4.22.tgz", + "integrity": "sha512-9Oxc0jQuppGLaQv5yaB2tVS2rAZzZ9NqDS1A4UlDLiYwJB7M6e89G6tmyOQjGjPwgoXPxQS4Vg2voSiKiED2gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz", + "integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==", + "license": "MIT", + "dependencies": { + "cron": "3.2.1", + "uuid": "11.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/swagger": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.17.14" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz", + "integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.37", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.37.tgz", + "integrity": "sha512-rDU6bkpuMs8YRt/UpkuYEAsYSoNuDEbrE41I3KNvmXREGH6DGBJ8Wbak4by29wNOQ27zk4g4HL82zf0OGhwRuw==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.105.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", + "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/services/transfer-service/package.json b/backend/services/transfer-service/package.json new file mode 100644 index 00000000..78766857 --- /dev/null +++ b/backend/services/transfer-service/package.json @@ -0,0 +1,95 @@ +{ + "name": "transfer-service", + "version": "1.0.0", + "description": "RWA Durian Queen Tree Transfer Service", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:migrate:prod": "prisma migrate deploy", + "prisma:studio": "prisma studio" + }, + "dependencies": { + "@nestjs/axios": "^3.0.1", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.0.0", + "@nestjs/swagger": "^7.1.17", + "@prisma/client": "^5.7.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "jsonwebtoken": "^9.0.0", + "kafkajs": "^2.2.4", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/jsonwebtoken": "^9.0.0", + "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.0", + "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "prisma": "^5.7.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^@/(.*)$": "/$1" + } + } +} diff --git a/backend/services/transfer-service/prisma/schema.prisma b/backend/services/transfer-service/prisma/schema.prisma new file mode 100644 index 00000000..11a6effe --- /dev/null +++ b/backend/services/transfer-service/prisma/schema.prisma @@ -0,0 +1,127 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================ +// 转让订单表 (Saga 聚合根) +// ============================================ +model TransferOrder { + id BigInt @id @default(autoincrement()) + transferOrderNo String @unique @map("transfer_order_no") @db.VarChar(50) + + // ========== 卖方信息 ========== + sellerUserId BigInt @map("seller_user_id") + sellerAccountSequence String @map("seller_account_sequence") @db.VarChar(20) + + // ========== 买方信息 ========== + buyerUserId BigInt @map("buyer_user_id") + buyerAccountSequence String @map("buyer_account_sequence") @db.VarChar(20) + + // ========== 转让标的 ========== + sourceOrderNo String @map("source_order_no") @db.VarChar(50) + sourceAdoptionId BigInt @map("source_adoption_id") + treeCount Int @map("tree_count") + contributionPerTree Decimal @map("contribution_per_tree") @db.Decimal(20, 10) + originalAdoptionDate DateTime @map("original_adoption_date") @db.Date + originalExpireDate DateTime @map("original_expire_date") @db.Date + selectedProvince String @map("selected_province") @db.VarChar(10) + selectedCity String @map("selected_city") @db.VarChar(10) + + // ========== 价格与费用 ========== + transferPrice Decimal @map("transfer_price") @db.Decimal(20, 8) + platformFeeRate Decimal @map("platform_fee_rate") @db.Decimal(5, 4) + platformFeeAmount Decimal @map("platform_fee_amount") @db.Decimal(20, 8) + sellerReceiveAmount Decimal @map("seller_receive_amount") @db.Decimal(20, 8) + + // ========== Saga 状态 ========== + status String @default("PENDING") @map("status") @db.VarChar(30) + sagaStep String @default("INIT") @map("saga_step") @db.VarChar(30) + failReason String? @map("fail_reason") @db.VarChar(500) + retryCount Int @default(0) @map("retry_count") + + // ========== 各步骤确认时间戳 ========== + sellerConfirmedAt DateTime? @map("seller_confirmed_at") + paymentFrozenAt DateTime? @map("payment_frozen_at") + treesLockedAt DateTime? @map("trees_locked_at") + ownershipTransferredAt DateTime? @map("ownership_transferred_at") + contributionAdjustedAt DateTime? @map("contribution_adjusted_at") + statsUpdatedAt DateTime? @map("stats_updated_at") + paymentSettledAt DateTime? @map("payment_settled_at") + completedAt DateTime? @map("completed_at") + cancelledAt DateTime? @map("cancelled_at") + rolledBackAt DateTime? @map("rolled_back_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + statusLogs TransferStatusLog[] + + @@index([sellerUserId]) + @@index([buyerUserId]) + @@index([sourceOrderNo]) + @@index([status]) + @@index([createdAt]) + @@map("transfer_orders") +} + +// ============================================ +// 状态变更日志表 (审计) +// ============================================ +model TransferStatusLog { + id BigInt @id @default(autoincrement()) + transferOrderNo String @map("transfer_order_no") @db.VarChar(50) + fromStatus String @map("from_status") @db.VarChar(30) + toStatus String @map("to_status") @db.VarChar(30) + fromSagaStep String @map("from_saga_step") @db.VarChar(30) + toSagaStep String @map("to_saga_step") @db.VarChar(30) + operatorType String @map("operator_type") @db.VarChar(20) + operatorId String? @map("operator_id") @db.VarChar(50) + remark String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + transferOrder TransferOrder @relation(fields: [transferOrderNo], references: [transferOrderNo]) + + @@index([transferOrderNo]) + @@map("transfer_status_logs") +} + +// ============================================ +// Outbox 事件表 (标准 Outbox Pattern) +// ============================================ +model OutboxEvent { + id BigInt @id @default(autoincrement()) + eventType String @map("event_type") @db.VarChar(100) + topic String @db.VarChar(200) + key String @db.VarChar(200) + payload Json + aggregateId String @map("aggregate_id") @db.VarChar(100) + aggregateType String @map("aggregate_type") @db.VarChar(100) + status String @default("PENDING") @db.VarChar(20) + retryCount Int @default(0) @map("retry_count") + maxRetries Int @default(5) @map("max_retries") + lastError String? @map("last_error") @db.VarChar(1000) + publishedAt DateTime? @map("published_at") + nextRetryAt DateTime? @map("next_retry_at") + createdAt DateTime @default(now()) @map("created_at") + + @@index([status, createdAt]) + @@index([status, nextRetryAt]) + @@map("outbox_events") +} + +// ============================================ +// 已处理事件表 (幂等性保证) +// ============================================ +model ProcessedEvent { + id BigInt @id @default(autoincrement()) + eventId String @unique @map("event_id") @db.VarChar(200) + eventType String @map("event_type") @db.VarChar(100) + processedAt DateTime @default(now()) @map("processed_at") + + @@map("processed_events") +} diff --git a/backend/services/transfer-service/src/api/api.module.ts b/backend/services/transfer-service/src/api/api.module.ts new file mode 100644 index 00000000..9d5466b9 --- /dev/null +++ b/backend/services/transfer-service/src/api/api.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TransferController } from './controllers/transfer.controller'; +import { AdminTransferController } from './controllers/admin-transfer.controller'; +import { HealthController } from './controllers/health.controller'; +import { ApplicationModule } from '../application/application.module'; + +@Module({ + imports: [ApplicationModule], + controllers: [ + TransferController, + AdminTransferController, + HealthController, + ], +}) +export class ApiModule {} diff --git a/backend/services/transfer-service/src/api/controllers/admin-transfer.controller.ts b/backend/services/transfer-service/src/api/controllers/admin-transfer.controller.ts new file mode 100644 index 00000000..872d57f4 --- /dev/null +++ b/backend/services/transfer-service/src/api/controllers/admin-transfer.controller.ts @@ -0,0 +1,114 @@ +import { + Controller, + Get, + Post, + Param, + Query, + NotFoundException, +} from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { TransferApplicationService } from '../../application/services/transfer-application.service'; +import { TransferOrderResponse } from '../dto/response/transfer-order.response'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; + +@ApiTags('管理端') +@Controller('admin/transfers') +export class AdminTransferController { + constructor( + private readonly transferService: TransferApplicationService, + private readonly prisma: PrismaService, + ) {} + + @Get() + @ApiOperation({ summary: '转让列表(管理端)' }) + async listTransfers( + @Query('status') status?: string, + @Query('limit') limit: number = 20, + @Query('offset') offset: number = 0, + ): Promise<{ total: number; items: any[] }> { + const where = status ? { status } : {}; + + const [total, items] = await Promise.all([ + this.prisma.transferOrder.count({ where }), + this.prisma.transferOrder.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: offset, + }), + ]); + + return { + total, + items: items.map((item) => ({ + ...item, + id: item.id.toString(), + sellerUserId: item.sellerUserId.toString(), + buyerUserId: item.buyerUserId.toString(), + sourceAdoptionId: item.sourceAdoptionId.toString(), + contributionPerTree: item.contributionPerTree.toString(), + transferPrice: item.transferPrice.toString(), + platformFeeAmount: item.platformFeeAmount.toString(), + sellerReceiveAmount: item.sellerReceiveAmount.toString(), + })), + }; + } + + @Get('stats') + @ApiOperation({ summary: '转让统计' }) + async getStats(): Promise { + const [total, pending, completed, cancelled, failed] = await Promise.all([ + this.prisma.transferOrder.count(), + this.prisma.transferOrder.count({ where: { status: 'PENDING' } }), + this.prisma.transferOrder.count({ where: { status: 'COMPLETED' } }), + this.prisma.transferOrder.count({ where: { status: 'CANCELLED' } }), + this.prisma.transferOrder.count({ where: { status: { in: ['FAILED', 'ROLLED_BACK'] } } }), + ]); + + return { total, pending, completed, cancelled, failed }; + } + + @Get(':transferOrderNo') + @ApiOperation({ summary: '转让详情(管理端)' }) + async getTransferDetail( + @Param('transferOrderNo') transferOrderNo: string, + ): Promise { + const order = await this.prisma.transferOrder.findUnique({ + where: { transferOrderNo }, + include: { statusLogs: { orderBy: { createdAt: 'asc' } } }, + }); + + if (!order) { + throw new NotFoundException(`转让订单 ${transferOrderNo} 不存在`); + } + + return { + ...order, + id: order.id.toString(), + sellerUserId: order.sellerUserId.toString(), + buyerUserId: order.buyerUserId.toString(), + sourceAdoptionId: order.sourceAdoptionId.toString(), + contributionPerTree: order.contributionPerTree.toString(), + transferPrice: order.transferPrice.toString(), + platformFeeAmount: order.platformFeeAmount.toString(), + sellerReceiveAmount: order.sellerReceiveAmount.toString(), + statusLogs: order.statusLogs.map((log) => ({ + ...log, + id: log.id.toString(), + })), + }; + } + + @Post(':transferOrderNo/force-cancel') + @ApiOperation({ summary: '强制取消转让(管理端)' }) + async forceCancel( + @Param('transferOrderNo') transferOrderNo: string, + ): Promise<{ success: boolean }> { + // 管理员强制取消,用系统用户ID + await this.transferService.cancelTransfer( + transferOrderNo, + BigInt(0), // 系统操作,需在 cancelTransfer 中增加管理员权限判断 + ); + return { success: true }; + } +} diff --git a/backend/services/transfer-service/src/api/controllers/health.controller.ts b/backend/services/transfer-service/src/api/controllers/health.controller.ts new file mode 100644 index 00000000..1ac6afdb --- /dev/null +++ b/backend/services/transfer-service/src/api/controllers/health.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; + +@ApiTags('健康检查') +@Controller('health') +export class HealthController { + constructor(private readonly prisma: PrismaService) {} + + @Get() + @ApiOperation({ summary: '健康检查' }) + async check(): Promise<{ status: string; service: string; timestamp: string }> { + // 检查数据库连接 + await this.prisma.$queryRaw`SELECT 1`; + + return { + status: 'ok', + service: 'transfer-service', + timestamp: new Date().toISOString(), + }; + } +} diff --git a/backend/services/transfer-service/src/api/controllers/transfer.controller.ts b/backend/services/transfer-service/src/api/controllers/transfer.controller.ts new file mode 100644 index 00000000..067e57d7 --- /dev/null +++ b/backend/services/transfer-service/src/api/controllers/transfer.controller.ts @@ -0,0 +1,94 @@ +import { + Controller, + Post, + Get, + Param, + Body, + Query, + UseGuards, + Request, + NotFoundException, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { TransferApplicationService } from '../../application/services/transfer-application.service'; +import { CreateTransferDto } from '../dto/request/create-transfer.dto'; +import { QueryTransfersDto } from '../dto/request/query-transfers.dto'; +import { TransferOrderResponse } from '../dto/response/transfer-order.response'; + +@ApiTags('转让订单') +@ApiBearerAuth() +@Controller('transfers') +@UseGuards(JwtAuthGuard) +export class TransferController { + constructor( + private readonly transferService: TransferApplicationService, + ) {} + + @Post() + @ApiOperation({ summary: '发起转让(买方向卖方购买)' }) + async createTransfer( + @Body() dto: CreateTransferDto, + @Request() req: any, + ): Promise<{ transferOrderNo: string }> { + return this.transferService.createTransferOrder({ + buyerUserId: BigInt(req.user.id), + buyerAccountSequence: req.user.accountSequence, + sourceOrderNo: dto.sourceOrderNo, + treeCount: dto.treeCount, + transferPrice: dto.transferPrice, + }); + } + + @Post(':transferOrderNo/seller-confirm') + @ApiOperation({ summary: '卖方确认转让' }) + async sellerConfirm( + @Param('transferOrderNo') transferOrderNo: string, + @Request() req: any, + ): Promise<{ success: boolean }> { + await this.transferService.sellerConfirm( + transferOrderNo, + BigInt(req.user.id), + ); + return { success: true }; + } + + @Post(':transferOrderNo/cancel') + @ApiOperation({ summary: '取消转让' }) + async cancelTransfer( + @Param('transferOrderNo') transferOrderNo: string, + @Request() req: any, + ): Promise<{ success: boolean }> { + await this.transferService.cancelTransfer( + transferOrderNo, + BigInt(req.user.id), + ); + return { success: true }; + } + + @Get() + @ApiOperation({ summary: '查询我的转让记录' }) + async getMyTransfers( + @Query() query: QueryTransfersDto, + @Request() req: any, + ): Promise { + const orders = await this.transferService.getMyTransfers( + BigInt(req.user.id), + query.role || 'all', + { limit: query.limit, offset: query.offset }, + ); + return orders.map(TransferOrderResponse.fromDomain); + } + + @Get(':transferOrderNo') + @ApiOperation({ summary: '查询转让详情' }) + async getTransferOrder( + @Param('transferOrderNo') transferOrderNo: string, + ): Promise { + const order = await this.transferService.getTransferOrder(transferOrderNo); + if (!order) { + throw new NotFoundException(`转让订单 ${transferOrderNo} 不存在`); + } + return TransferOrderResponse.fromDomain(order); + } +} diff --git a/backend/services/transfer-service/src/api/dto/request/create-transfer.dto.ts b/backend/services/transfer-service/src/api/dto/request/create-transfer.dto.ts new file mode 100644 index 00000000..402e6cb0 --- /dev/null +++ b/backend/services/transfer-service/src/api/dto/request/create-transfer.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsNumber, IsNotEmpty, Min, IsDecimal } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTransferDto { + @ApiProperty({ description: '原认种订单号', example: 'PLT260101A1B2C3D4' }) + @IsString() + @IsNotEmpty() + sourceOrderNo: string; + + @ApiProperty({ description: '转让棵数', example: 1 }) + @IsNumber() + @Min(1) + treeCount: number; + + @ApiProperty({ description: '转让总价 (USDT)', example: '15000.00000000' }) + @IsString() + @IsNotEmpty() + transferPrice: string; +} diff --git a/backend/services/transfer-service/src/api/dto/request/query-transfers.dto.ts b/backend/services/transfer-service/src/api/dto/request/query-transfers.dto.ts new file mode 100644 index 00000000..bac0cfeb --- /dev/null +++ b/backend/services/transfer-service/src/api/dto/request/query-transfers.dto.ts @@ -0,0 +1,21 @@ +import { IsOptional, IsString, IsNumber, Min } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class QueryTransfersDto { + @ApiPropertyOptional({ description: '角色筛选', enum: ['buyer', 'seller', 'all'], default: 'all' }) + @IsOptional() + @IsString() + role?: 'buyer' | 'seller' | 'all'; + + @ApiPropertyOptional({ description: '每页条数', default: 20 }) + @IsOptional() + @IsNumber() + @Min(1) + limit?: number; + + @ApiPropertyOptional({ description: '偏移量', default: 0 }) + @IsOptional() + @IsNumber() + @Min(0) + offset?: number; +} diff --git a/backend/services/transfer-service/src/api/dto/response/transfer-order.response.ts b/backend/services/transfer-service/src/api/dto/response/transfer-order.response.ts new file mode 100644 index 00000000..81a4d497 --- /dev/null +++ b/backend/services/transfer-service/src/api/dto/response/transfer-order.response.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TransferOrder } from '../../../domain/aggregates/transfer-order.aggregate'; + +export class TransferOrderResponse { + @ApiProperty() transferOrderNo: string; + @ApiProperty() sellerAccountSequence: string; + @ApiProperty() buyerAccountSequence: string; + @ApiProperty() sourceOrderNo: string; + @ApiProperty() treeCount: number; + @ApiProperty() transferPrice: string; + @ApiProperty() platformFeeAmount: string; + @ApiProperty() sellerReceiveAmount: string; + @ApiProperty() status: string; + @ApiProperty() selectedProvince: string; + @ApiProperty() selectedCity: string; + @ApiProperty() createdAt: string; + @ApiProperty({ required: false }) sellerConfirmedAt?: string; + @ApiProperty({ required: false }) completedAt?: string; + @ApiProperty({ required: false }) cancelledAt?: string; + + static fromDomain(order: TransferOrder): TransferOrderResponse { + const response = new TransferOrderResponse(); + response.transferOrderNo = order.transferOrderNo; + response.sellerAccountSequence = order.sellerAccountSequence; + response.buyerAccountSequence = order.buyerAccountSequence; + response.sourceOrderNo = order.sourceOrderNo; + response.treeCount = order.treeCount; + response.transferPrice = order.transferPrice; + response.platformFeeAmount = order.platformFeeAmount; + response.sellerReceiveAmount = order.sellerReceiveAmount; + response.status = order.status; + response.selectedProvince = order.selectedProvince; + response.selectedCity = order.selectedCity; + response.createdAt = order.createdAt.toISOString(); + response.sellerConfirmedAt = order.sellerConfirmedAt?.toISOString(); + response.completedAt = order.completedAt?.toISOString(); + response.cancelledAt = order.cancelledAt?.toISOString(); + return response; + } +} diff --git a/backend/services/transfer-service/src/api/guards/jwt-auth.guard.ts b/backend/services/transfer-service/src/api/guards/jwt-auth.guard.ts new file mode 100644 index 00000000..a637c961 --- /dev/null +++ b/backend/services/transfer-service/src/api/guards/jwt-auth.guard.ts @@ -0,0 +1,52 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as jwt from 'jsonwebtoken'; + +export interface JwtPayload { + sub: string; + userId: string; + accountSequence: string; + iat: number; + exp: number; +} + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private readonly configService: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('Missing authentication token'); + } + + try { + const secret = this.configService.get('JWT_SECRET'); + if (!secret) { + throw new Error('JWT_SECRET not configured'); + } + + const payload = jwt.verify(token, secret) as JwtPayload; + request.user = { + id: payload.userId || payload.sub, + accountSequence: payload.accountSequence, + }; + + return true; + } catch (error) { + throw new UnauthorizedException('Invalid authentication token'); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/backend/services/transfer-service/src/api/index.ts b/backend/services/transfer-service/src/api/index.ts new file mode 100644 index 00000000..d2444b70 --- /dev/null +++ b/backend/services/transfer-service/src/api/index.ts @@ -0,0 +1 @@ +export * from './api.module'; diff --git a/backend/services/transfer-service/src/app.module.ts b/backend/services/transfer-service/src/app.module.ts new file mode 100644 index 00000000..e5683034 --- /dev/null +++ b/backend/services/transfer-service/src/app.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER } from '@nestjs/core'; +import { InfrastructureModule } from './infrastructure/infrastructure.module'; +import { DomainModule } from './domain/domain.module'; +import { ApplicationModule } from './application/application.module'; +import { ApiModule } from './api/api.module'; +import { GlobalExceptionFilter } from './shared/filters/global-exception.filter'; +import configs from './config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.development', '.env'], + load: configs, + }), + InfrastructureModule, + DomainModule, + ApplicationModule, + ApiModule, + ], + providers: [ + { + provide: APP_FILTER, + useClass: GlobalExceptionFilter, + }, + ], +}) +export class AppModule {} diff --git a/backend/services/transfer-service/src/application/application.module.ts b/backend/services/transfer-service/src/application/application.module.ts new file mode 100644 index 00000000..79e0bc26 --- /dev/null +++ b/backend/services/transfer-service/src/application/application.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DomainModule } from '../domain/domain.module'; +import { TransferApplicationService } from './services/transfer-application.service'; +import { SagaOrchestratorService } from './services/saga-orchestrator.service'; + +@Module({ + imports: [DomainModule, ScheduleModule.forRoot()], + providers: [ + TransferApplicationService, + SagaOrchestratorService, + ], + exports: [ + TransferApplicationService, + SagaOrchestratorService, + ], +}) +export class ApplicationModule {} diff --git a/backend/services/transfer-service/src/application/index.ts b/backend/services/transfer-service/src/application/index.ts new file mode 100644 index 00000000..2b3b9256 --- /dev/null +++ b/backend/services/transfer-service/src/application/index.ts @@ -0,0 +1,3 @@ +export * from './application.module'; +export * from './services/transfer-application.service'; +export * from './services/saga-orchestrator.service'; diff --git a/backend/services/transfer-service/src/application/services/saga-orchestrator.service.ts b/backend/services/transfer-service/src/application/services/saga-orchestrator.service.ts new file mode 100644 index 00000000..9cf5b85d --- /dev/null +++ b/backend/services/transfer-service/src/application/services/saga-orchestrator.service.ts @@ -0,0 +1,312 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { + ITransferOrderRepository, + TRANSFER_ORDER_REPOSITORY, +} from '../../domain/repositories/transfer-order.repository.interface'; +import { TransferOrderStatus } from '../../domain/value-objects/transfer-order-status.enum'; +import { SagaStep } from '../../domain/value-objects/saga-step.enum'; +import { UnitOfWork, UNIT_OF_WORK } from '../../infrastructure/persistence/unit-of-work'; +import { EventPublisherService } from '../../infrastructure/kafka/event-publisher.service'; +import { WalletServiceClient } from '../../infrastructure/external/wallet-service.client'; +import { DomainEvent } from '../../domain/events/domain-event.interface'; + +/** + * Saga 编排器 + * 负责推进转让订单的各个步骤 + * + * 状态机流转: + * SELLER_CONFIRMED → 冻结买方资金 → PAYMENT_FROZEN + * PAYMENT_FROZEN → 锁定卖方树 → TREES_LOCKED + * TREES_LOCKED → 变更所有权 → OWNERSHIP_TRANSFERRED + * OWNERSHIP_TRANSFERRED → 调整算力 → CONTRIBUTION_ADJUSTED + * CONTRIBUTION_ADJUSTED → 更新团队统计 → STATS_UPDATED + * STATS_UPDATED → 结算资金 → PAYMENT_SETTLED → COMPLETED + */ +@Injectable() +export class SagaOrchestratorService { + private readonly logger = new Logger(SagaOrchestratorService.name); + + constructor( + @Inject(TRANSFER_ORDER_REPOSITORY) + private readonly transferOrderRepo: ITransferOrderRepository, + @Inject(UNIT_OF_WORK) + private readonly unitOfWork: UnitOfWork, + private readonly eventPublisher: EventPublisherService, + private readonly walletClient: WalletServiceClient, + ) {} + + /** + * 推进 Saga:卖方确认后 → 冻结买方资金 + */ + async proceedAfterSellerConfirm(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Proceeding after seller confirm: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.SELLER_CONFIRMED) return; + + try { + // 调用 wallet-service 冻结买方资金 + await this.walletClient.freezePayment({ + accountSequence: order.buyerAccountSequence, + amount: order.transferPrice, + transferOrderNo: order.transferOrderNo, + reason: `树转让冻结 - ${order.transferOrderNo}`, + }); + + order.markPaymentFrozen(); + + await this.saveAndPublish(order, 'SYSTEM', '买方资金冻结成功'); + + // 继续下一步:发送锁定树事件 + await this.proceedToLockTrees(order.transferOrderNo); + } catch (error) { + this.logger.error(`[SAGA] Failed to freeze payment for ${transferOrderNo}:`, error); + await this.handleFailure(transferOrderNo, `冻结资金失败: ${(error as Error).message}`); + } + } + + /** + * 推进 Saga:资金冻结后 → 锁定卖方树 + */ + async proceedToLockTrees(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Proceeding to lock trees: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.PAYMENT_FROZEN) return; + + // 发送 Kafka 事件给 planting-service 锁定树 + await this.eventPublisher.publish('transfer.trees.lock', transferOrderNo, { + transferOrderNo: order.transferOrderNo, + sellerUserId: order.sellerUserId.toString(), + sourceOrderNo: order.sourceOrderNo, + treeCount: order.treeCount, + sellerAccountSequence: order.sellerAccountSequence, + }); + } + + /** + * 收到树锁定确认 → 变更所有权 + */ + async handleTreesLockedAck(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Trees locked ack: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.PAYMENT_FROZEN) return; + + order.markTreesLocked(); + await this.saveAndPublish(order, 'SYSTEM', '卖方树锁定成功'); + + // 继续:发送所有权变更事件 + await this.eventPublisher.publish('transfer.ownership.execute', transferOrderNo, { + transferOrderNo: order.transferOrderNo, + sellerUserId: order.sellerUserId.toString(), + sellerAccountSequence: order.sellerAccountSequence, + buyerUserId: order.buyerUserId.toString(), + buyerAccountSequence: order.buyerAccountSequence, + sourceOrderNo: order.sourceOrderNo, + treeCount: order.treeCount, + contributionPerTree: order.contributionPerTree, + selectedProvince: order.selectedProvince, + selectedCity: order.selectedCity, + originalAdoptionDate: order.originalAdoptionDate.toISOString(), + originalExpireDate: order.originalExpireDate.toISOString(), + }); + } + + /** + * 收到所有权变更完成确认 → 等待算力调整 + */ + async handleOwnershipTransferred(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Ownership transferred: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.TREES_LOCKED) return; + + order.markOwnershipTransferred(); + await this.saveAndPublish(order, 'SYSTEM', '所有权变更完成'); + + // planting-service 发出的 planting.ownership.removed / planting.ownership.added + // 会被 contribution-service 和 referral-service 各自消费 + // 它们处理完后会发送确认事件回来 + } + + /** + * 收到算力调整完成确认 + */ + async handleContributionAdjusted(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Contribution adjusted: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.OWNERSHIP_TRANSFERRED) return; + + order.markContributionAdjusted(); + await this.saveAndPublish(order, 'SYSTEM', '算力调整完成'); + } + + /** + * 收到团队统计更新完成确认 + */ + async handleStatsUpdated(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Stats updated: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.CONTRIBUTION_ADJUSTED) return; + + order.markStatsUpdated(); + await this.saveAndPublish(order, 'SYSTEM', '团队统计更新完成'); + + // 继续:结算资金 + await this.proceedToSettlePayment(order.transferOrderNo); + } + + /** + * 结算资金 → 完成 + */ + async proceedToSettlePayment(transferOrderNo: string): Promise { + this.logger.log(`[SAGA] Proceeding to settle payment: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.STATS_UPDATED) return; + + try { + await this.walletClient.settleTransferPayment({ + buyerAccountSequence: order.buyerAccountSequence, + sellerAccountSequence: order.sellerAccountSequence, + freezeId: order.transferOrderNo, // 使用订单号作为冻结标识 + sellerReceiveAmount: order.sellerReceiveAmount, + platformFeeAmount: order.platformFeeAmount, + transferOrderNo: order.transferOrderNo, + }); + + order.markPaymentSettled(); + await this.saveAndPublish(order, 'SYSTEM', '资金结算完成'); + + // 完成转让 + order.complete(); + await this.saveAndPublish(order, 'SYSTEM', '转让完成'); + } catch (error) { + this.logger.error(`[SAGA] Failed to settle payment for ${transferOrderNo}:`, error); + await this.handleFailure(transferOrderNo, `资金结算失败: ${(error as Error).message}`); + } + } + + /** + * 处理失败 → 开始补偿 + */ + async handleFailure(transferOrderNo: string, reason: string): Promise { + this.logger.error(`[SAGA] Handling failure for ${transferOrderNo}: ${reason}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order) return; + + order.markFailed(reason); + + await this.saveAndPublish(order, 'SYSTEM', `转让失败: ${reason}`); + + // 如果需要补偿,开始补偿流程 + if (order.status === TransferOrderStatus.ROLLING_BACK) { + await this.executeCompensation(order.transferOrderNo); + } + } + + /** + * 执行补偿操作 + */ + private async executeCompensation(transferOrderNo: string): Promise { + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.ROLLING_BACK) return; + + if (order.sagaStep === SagaStep.COMPENSATE_UNLOCK_TREES) { + // 发送解锁树事件 + await this.eventPublisher.publish('transfer.trees.unlock', transferOrderNo, { + transferOrderNo: order.transferOrderNo, + sourceOrderNo: order.sourceOrderNo, + treeCount: order.treeCount, + }); + } else if (order.sagaStep === SagaStep.COMPENSATE_UNFREEZE) { + // 解冻资金 + try { + await this.walletClient.unfreezePayment({ + accountSequence: order.buyerAccountSequence, + freezeId: order.transferOrderNo, + transferOrderNo: order.transferOrderNo, + reason: `转让取消解冻 - ${order.transferOrderNo}`, + }); + + order.markRolledBack(); + await this.saveAndPublish(order, 'SYSTEM', '补偿完成:资金已解冻'); + } catch (error) { + this.logger.error(`[SAGA] Failed to unfreeze for ${transferOrderNo}:`, error); + } + } + } + + /** + * 收到树解锁确认 → 继续补偿 + */ + async handleTreesUnlockedAck(transferOrderNo: string): Promise { + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order || order.status !== TransferOrderStatus.ROLLING_BACK) return; + + order.advanceCompensation(); + + await this.saveAndPublish(order, 'SYSTEM', '补偿:树已解锁'); + + // 如果还需要解冻资金 + if (order.status === TransferOrderStatus.ROLLING_BACK) { + await this.executeCompensation(order.transferOrderNo); + } + } + + /** + * 保存订单并发布事件 + */ + private async saveAndPublish( + order: any, + operatorType: string, + remark: string, + ): Promise { + await this.unitOfWork.executeInTransaction(async (uow) => { + await uow.saveTransferOrder(order); + + const events = order.getDomainEvents() as DomainEvent[]; + if (events.length > 0) { + const lastEvent = events[events.length - 1]; + if (lastEvent.type === 'TransferStatusChanged') { + const data = lastEvent.data as any; + await uow.logStatusChange({ + transferOrderNo: order.transferOrderNo, + fromStatus: data.fromStatus, + toStatus: data.toStatus, + fromSagaStep: data.fromSagaStep, + toSagaStep: data.toSagaStep, + operatorType, + remark, + }); + } + } + + const outboxEvents = events.map((e: DomainEvent) => ({ + eventType: e.type, + topic: this.getTopicForEvent(e.type), + key: e.aggregateId, + payload: e.data, + aggregateId: e.aggregateId, + aggregateType: e.aggregateType, + })); + uow.addOutboxEvents(outboxEvents); + await uow.commitOutboxEvents(); + }); + + order.clearDomainEvents(); + } + + private getTopicForEvent(eventType: string): string { + const topicMap: Record = { + TransferOrderCreated: 'transfer.order.created', + TransferStatusChanged: 'transfer.status.changed', + TransferCompleted: 'transfer.order.completed', + }; + return topicMap[eventType] || 'transfer.events'; + } +} diff --git a/backend/services/transfer-service/src/application/services/transfer-application.service.ts b/backend/services/transfer-service/src/application/services/transfer-application.service.ts new file mode 100644 index 00000000..668d4d42 --- /dev/null +++ b/backend/services/transfer-service/src/application/services/transfer-application.service.ts @@ -0,0 +1,268 @@ +import { Injectable, Logger, Inject, BadRequestException, NotFoundException } from '@nestjs/common'; +import { + TransferOrder, + CreateTransferOrderParams, +} from '../../domain/aggregates/transfer-order.aggregate'; +import { + ITransferOrderRepository, + TRANSFER_ORDER_REPOSITORY, +} from '../../domain/repositories/transfer-order.repository.interface'; +import { TransferFeeDomainService } from '../../domain/services/transfer-fee.service'; +import { TransferOrderStatus, CANCELLABLE_STATUSES } from '../../domain/value-objects/transfer-order-status.enum'; +import { UnitOfWork, UNIT_OF_WORK } from '../../infrastructure/persistence/unit-of-work'; +import { PlantingServiceClient } from '../../infrastructure/external/planting-service.client'; +import { IdentityServiceClient } from '../../infrastructure/external/identity-service.client'; +import { WalletServiceClient } from '../../infrastructure/external/wallet-service.client'; +import { DomainEvent } from '../../domain/events/domain-event.interface'; + +@Injectable() +export class TransferApplicationService { + private readonly logger = new Logger(TransferApplicationService.name); + + constructor( + @Inject(TRANSFER_ORDER_REPOSITORY) + private readonly transferOrderRepo: ITransferOrderRepository, + @Inject(UNIT_OF_WORK) + private readonly unitOfWork: UnitOfWork, + private readonly transferFeeService: TransferFeeDomainService, + private readonly plantingClient: PlantingServiceClient, + private readonly identityClient: IdentityServiceClient, + private readonly walletClient: WalletServiceClient, + ) {} + + /** + * 发起转让(买方向卖方购买) + */ + async createTransferOrder(params: { + buyerUserId: bigint; + buyerAccountSequence: string; + sourceOrderNo: string; + treeCount: number; + transferPrice: string; + }): Promise<{ transferOrderNo: string }> { + this.logger.log(`Creating transfer order: buyer=${params.buyerAccountSequence}, source=${params.sourceOrderNo}, trees=${params.treeCount}`); + + // 1. 查询原订单信息 + const plantingOrder = await this.plantingClient.getPlantingOrder(params.sourceOrderNo); + if (!plantingOrder) { + throw new NotFoundException(`认种订单 ${params.sourceOrderNo} 不存在`); + } + + // 2. 验证订单状态 + if (plantingOrder.status !== 'MINING_ENABLED') { + throw new BadRequestException(`订单 ${params.sourceOrderNo} 状态不允许转让,当前状态: ${plantingOrder.status}`); + } + + // 3. 验证可转让棵数 + if (plantingOrder.availableForTransfer < params.treeCount) { + throw new BadRequestException( + `可转让棵数不足,可转让: ${plantingOrder.availableForTransfer},请求: ${params.treeCount}`, + ); + } + + // 4. 不能自己转给自己 + if (params.buyerUserId === BigInt(plantingOrder.userId)) { + throw new BadRequestException('不能转让给自己'); + } + + // 5. 验证买方实名认证 + const buyerKyc = await this.identityClient.isKycVerified(params.buyerUserId.toString()); + if (!buyerKyc) { + throw new BadRequestException('买方尚未完成实名认证'); + } + + // 6. 查询原始算力值 + const contributionInfo = await this.plantingClient.getContributionPerTree(params.sourceOrderNo); + + // 7. 计算费用 + const fees = this.transferFeeService.calculateFees(params.transferPrice); + + // 8. 创建聚合根 + const createParams: CreateTransferOrderParams = { + sellerUserId: BigInt(plantingOrder.userId), + sellerAccountSequence: plantingOrder.accountSequence, + buyerUserId: params.buyerUserId, + buyerAccountSequence: params.buyerAccountSequence, + sourceOrderNo: params.sourceOrderNo, + sourceAdoptionId: BigInt(plantingOrder.id), + treeCount: params.treeCount, + contributionPerTree: contributionInfo.contributionPerTree, + originalAdoptionDate: new Date(contributionInfo.adoptionDate), + originalExpireDate: new Date(contributionInfo.expireDate), + selectedProvince: plantingOrder.selectedProvince, + selectedCity: plantingOrder.selectedCity, + transferPrice: params.transferPrice, + platformFeeRate: fees.platformFeeRate, + platformFeeAmount: fees.platformFeeAmount, + sellerReceiveAmount: fees.sellerReceiveAmount, + }; + + const order = TransferOrder.create(createParams); + + // 9. 事务保存 + await this.unitOfWork.executeInTransaction(async (uow) => { + await uow.saveTransferOrder(order); + + // 记录状态日志 + await uow.logStatusChange({ + transferOrderNo: order.transferOrderNo, + fromStatus: '', + toStatus: TransferOrderStatus.PENDING, + fromSagaStep: '', + toSagaStep: 'INIT', + operatorType: 'USER', + operatorId: params.buyerUserId.toString(), + remark: '买方发起转让', + }); + + // Outbox 事件 + const outboxEvents = order.getDomainEvents().map((e: DomainEvent) => ({ + eventType: e.type, + topic: this.getTopicForEvent(e.type), + key: e.aggregateId, + payload: e.data, + aggregateId: e.aggregateId, + aggregateType: e.aggregateType, + })); + uow.addOutboxEvents(outboxEvents); + await uow.commitOutboxEvents(); + }); + + order.clearDomainEvents(); + this.logger.log(`Transfer order created: ${order.transferOrderNo}`); + + return { transferOrderNo: order.transferOrderNo }; + } + + /** + * 卖方确认转让 + */ + async sellerConfirm(transferOrderNo: string, sellerUserId: bigint): Promise { + this.logger.log(`Seller confirming transfer: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order) { + throw new NotFoundException(`转让订单 ${transferOrderNo} 不存在`); + } + + if (order.sellerUserId !== sellerUserId) { + throw new BadRequestException('仅卖方可以确认'); + } + + order.sellerConfirm(); + + await this.unitOfWork.executeInTransaction(async (uow) => { + await uow.saveTransferOrder(order); + + await uow.logStatusChange({ + transferOrderNo: order.transferOrderNo, + fromStatus: TransferOrderStatus.PENDING, + toStatus: TransferOrderStatus.SELLER_CONFIRMED, + fromSagaStep: 'INIT', + toSagaStep: 'FREEZE_BUYER_PAYMENT', + operatorType: 'USER', + operatorId: sellerUserId.toString(), + remark: '卖方确认转让', + }); + + const outboxEvents = order.getDomainEvents().map((e: DomainEvent) => ({ + eventType: e.type, + topic: this.getTopicForEvent(e.type), + key: e.aggregateId, + payload: e.data, + aggregateId: e.aggregateId, + aggregateType: e.aggregateType, + })); + uow.addOutboxEvents(outboxEvents); + await uow.commitOutboxEvents(); + }); + + order.clearDomainEvents(); + } + + /** + * 取消转让 + */ + async cancelTransfer(transferOrderNo: string, userId: bigint): Promise { + this.logger.log(`Cancelling transfer: ${transferOrderNo}`); + + const order = await this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + if (!order) { + throw new NotFoundException(`转让订单 ${transferOrderNo} 不存在`); + } + + // 买方或卖方都可取消 + if (order.sellerUserId !== userId && order.buyerUserId !== userId) { + throw new BadRequestException('仅买方或卖方可以取消'); + } + + order.cancel(); + + await this.unitOfWork.executeInTransaction(async (uow) => { + await uow.saveTransferOrder(order); + + await uow.logStatusChange({ + transferOrderNo: order.transferOrderNo, + fromStatus: order.status, + toStatus: TransferOrderStatus.CANCELLED, + fromSagaStep: order.sagaStep, + toSagaStep: order.sagaStep, + operatorType: 'USER', + operatorId: userId.toString(), + remark: '用户取消转让', + }); + + const outboxEvents = order.getDomainEvents().map((e: DomainEvent) => ({ + eventType: e.type, + topic: this.getTopicForEvent(e.type), + key: e.aggregateId, + payload: e.data, + aggregateId: e.aggregateId, + aggregateType: e.aggregateType, + })); + uow.addOutboxEvents(outboxEvents); + await uow.commitOutboxEvents(); + }); + + order.clearDomainEvents(); + } + + /** + * 查询转让订单详情 + */ + async getTransferOrder(transferOrderNo: string): Promise { + return this.transferOrderRepo.findByTransferOrderNo(transferOrderNo); + } + + /** + * 查询我的转让记录(作为买方或卖方) + */ + async getMyTransfers( + userId: bigint, + role: 'buyer' | 'seller' | 'all', + options?: { limit?: number; offset?: number }, + ): Promise { + if (role === 'buyer') { + return this.transferOrderRepo.findByBuyerUserId(userId, options); + } else if (role === 'seller') { + return this.transferOrderRepo.findBySellerUserId(userId, options); + } else { + const [buyerOrders, sellerOrders] = await Promise.all([ + this.transferOrderRepo.findByBuyerUserId(userId, options), + this.transferOrderRepo.findBySellerUserId(userId, options), + ]); + return [...buyerOrders, ...sellerOrders].sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), + ); + } + } + + private getTopicForEvent(eventType: string): string { + const topicMap: Record = { + TransferOrderCreated: 'transfer.order.created', + TransferStatusChanged: 'transfer.status.changed', + TransferCompleted: 'transfer.order.completed', + }; + return topicMap[eventType] || 'transfer.events'; + } +} diff --git a/backend/services/transfer-service/src/config/app.config.ts b/backend/services/transfer-service/src/config/app.config.ts new file mode 100644 index 00000000..68d819bf --- /dev/null +++ b/backend/services/transfer-service/src/config/app.config.ts @@ -0,0 +1,7 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('app', () => ({ + nodeEnv: process.env.NODE_ENV || 'development', + port: parseInt(process.env.APP_PORT || '3013', 10), + serviceName: 'transfer-service', +})); diff --git a/backend/services/transfer-service/src/config/external.config.ts b/backend/services/transfer-service/src/config/external.config.ts new file mode 100644 index 00000000..c27aad57 --- /dev/null +++ b/backend/services/transfer-service/src/config/external.config.ts @@ -0,0 +1,14 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('external', () => ({ + walletServiceUrl: + process.env.WALLET_SERVICE_URL || 'http://localhost:3001', + identityServiceUrl: + process.env.IDENTITY_SERVICE_URL || 'http://localhost:3000', + plantingServiceUrl: + process.env.PLANTING_SERVICE_URL || 'http://localhost:3003', + referralServiceUrl: + process.env.REFERRAL_SERVICE_URL || 'http://localhost:3004', + contributionServiceUrl: + process.env.CONTRIBUTION_SERVICE_URL || 'http://localhost:3020', +})); diff --git a/backend/services/transfer-service/src/config/index.ts b/backend/services/transfer-service/src/config/index.ts new file mode 100644 index 00000000..9c7a1257 --- /dev/null +++ b/backend/services/transfer-service/src/config/index.ts @@ -0,0 +1,5 @@ +import appConfig from './app.config'; +import jwtConfig from './jwt.config'; +import externalConfig from './external.config'; + +export default [appConfig, jwtConfig, externalConfig]; diff --git a/backend/services/transfer-service/src/config/jwt.config.ts b/backend/services/transfer-service/src/config/jwt.config.ts new file mode 100644 index 00000000..828f1043 --- /dev/null +++ b/backend/services/transfer-service/src/config/jwt.config.ts @@ -0,0 +1,5 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('jwt', () => ({ + secret: process.env.JWT_SECRET || 'default-secret-change-me', +})); diff --git a/backend/services/transfer-service/src/domain/aggregates/transfer-order.aggregate.ts b/backend/services/transfer-service/src/domain/aggregates/transfer-order.aggregate.ts new file mode 100644 index 00000000..3ebd055b --- /dev/null +++ b/backend/services/transfer-service/src/domain/aggregates/transfer-order.aggregate.ts @@ -0,0 +1,608 @@ +import { DomainEvent } from '../events/domain-event.interface'; +import { TransferOrderCreatedEvent } from '../events/transfer-order-created.event'; +import { TransferStatusChangedEvent } from '../events/transfer-status-changed.event'; +import { TransferCompletedEvent } from '../events/transfer-completed.event'; +import { + TransferOrderStatus, + CANCELLABLE_STATUSES, + TERMINAL_STATUSES, +} from '../value-objects/transfer-order-status.enum'; +import { SagaStep } from '../value-objects/saga-step.enum'; +import { v4 as uuidv4 } from 'uuid'; + +export interface TransferOrderData { + id: bigint | null; + transferOrderNo: string; + sellerUserId: bigint; + sellerAccountSequence: string; + buyerUserId: bigint; + buyerAccountSequence: string; + sourceOrderNo: string; + sourceAdoptionId: bigint; + treeCount: number; + contributionPerTree: string; + originalAdoptionDate: Date; + originalExpireDate: Date; + selectedProvince: string; + selectedCity: string; + transferPrice: string; + platformFeeRate: string; + platformFeeAmount: string; + sellerReceiveAmount: string; + status: string; + sagaStep: string; + failReason: string | null; + retryCount: number; + sellerConfirmedAt: Date | null; + paymentFrozenAt: Date | null; + treesLockedAt: Date | null; + ownershipTransferredAt: Date | null; + contributionAdjustedAt: Date | null; + statsUpdatedAt: Date | null; + paymentSettledAt: Date | null; + completedAt: Date | null; + cancelledAt: Date | null; + rolledBackAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateTransferOrderParams { + sellerUserId: bigint; + sellerAccountSequence: string; + buyerUserId: bigint; + buyerAccountSequence: string; + sourceOrderNo: string; + sourceAdoptionId: bigint; + treeCount: number; + contributionPerTree: string; + originalAdoptionDate: Date; + originalExpireDate: Date; + selectedProvince: string; + selectedCity: string; + transferPrice: string; + platformFeeRate: string; + platformFeeAmount: string; + sellerReceiveAmount: string; +} + +export class TransferOrder { + private _id: bigint | null; + private readonly _transferOrderNo: string; + private readonly _sellerUserId: bigint; + private readonly _sellerAccountSequence: string; + private readonly _buyerUserId: bigint; + private readonly _buyerAccountSequence: string; + private readonly _sourceOrderNo: string; + private readonly _sourceAdoptionId: bigint; + private readonly _treeCount: number; + private readonly _contributionPerTree: string; + private readonly _originalAdoptionDate: Date; + private readonly _originalExpireDate: Date; + private readonly _selectedProvince: string; + private readonly _selectedCity: string; + private readonly _transferPrice: string; + private readonly _platformFeeRate: string; + private readonly _platformFeeAmount: string; + private readonly _sellerReceiveAmount: string; + private _status: TransferOrderStatus; + private _sagaStep: SagaStep; + private _failReason: string | null; + private _retryCount: number; + private _sellerConfirmedAt: Date | null; + private _paymentFrozenAt: Date | null; + private _treesLockedAt: Date | null; + private _ownershipTransferredAt: Date | null; + private _contributionAdjustedAt: Date | null; + private _statsUpdatedAt: Date | null; + private _paymentSettledAt: Date | null; + private _completedAt: Date | null; + private _cancelledAt: Date | null; + private _rolledBackAt: Date | null; + private _createdAt: Date; + private _updatedAt: Date; + private _domainEvents: DomainEvent[] = []; + + private constructor(params: { + id: bigint | null; + transferOrderNo: string; + sellerUserId: bigint; + sellerAccountSequence: string; + buyerUserId: bigint; + buyerAccountSequence: string; + sourceOrderNo: string; + sourceAdoptionId: bigint; + treeCount: number; + contributionPerTree: string; + originalAdoptionDate: Date; + originalExpireDate: Date; + selectedProvince: string; + selectedCity: string; + transferPrice: string; + platformFeeRate: string; + platformFeeAmount: string; + sellerReceiveAmount: string; + status: TransferOrderStatus; + sagaStep: SagaStep; + failReason: string | null; + retryCount: number; + sellerConfirmedAt: Date | null; + paymentFrozenAt: Date | null; + treesLockedAt: Date | null; + ownershipTransferredAt: Date | null; + contributionAdjustedAt: Date | null; + statsUpdatedAt: Date | null; + paymentSettledAt: Date | null; + completedAt: Date | null; + cancelledAt: Date | null; + rolledBackAt: Date | null; + createdAt: Date; + updatedAt: Date; + }) { + this._id = params.id; + this._transferOrderNo = params.transferOrderNo; + this._sellerUserId = params.sellerUserId; + this._sellerAccountSequence = params.sellerAccountSequence; + this._buyerUserId = params.buyerUserId; + this._buyerAccountSequence = params.buyerAccountSequence; + this._sourceOrderNo = params.sourceOrderNo; + this._sourceAdoptionId = params.sourceAdoptionId; + this._treeCount = params.treeCount; + this._contributionPerTree = params.contributionPerTree; + this._originalAdoptionDate = params.originalAdoptionDate; + this._originalExpireDate = params.originalExpireDate; + this._selectedProvince = params.selectedProvince; + this._selectedCity = params.selectedCity; + this._transferPrice = params.transferPrice; + this._platformFeeRate = params.platformFeeRate; + this._platformFeeAmount = params.platformFeeAmount; + this._sellerReceiveAmount = params.sellerReceiveAmount; + this._status = params.status; + this._sagaStep = params.sagaStep; + this._failReason = params.failReason; + this._retryCount = params.retryCount; + this._sellerConfirmedAt = params.sellerConfirmedAt; + this._paymentFrozenAt = params.paymentFrozenAt; + this._treesLockedAt = params.treesLockedAt; + this._ownershipTransferredAt = params.ownershipTransferredAt; + this._contributionAdjustedAt = params.contributionAdjustedAt; + this._statsUpdatedAt = params.statsUpdatedAt; + this._paymentSettledAt = params.paymentSettledAt; + this._completedAt = params.completedAt; + this._cancelledAt = params.cancelledAt; + this._rolledBackAt = params.rolledBackAt; + this._createdAt = params.createdAt; + this._updatedAt = params.updatedAt; + } + + // ========== 工厂方法 ========== + + /** + * 生成转让单号:TRF + YYMMDD + 8位随机字符 + */ + private static generateTransferOrderNo(): string { + const now = new Date(); + const yy = String(now.getFullYear()).slice(2); + const mm = String(now.getMonth() + 1).padStart(2, '0'); + const dd = String(now.getDate()).padStart(2, '0'); + const random = uuidv4().replace(/-/g, '').substring(0, 8).toUpperCase(); + return `TRF${yy}${mm}${dd}${random}`; + } + + /** + * 创建新的转让订单 + */ + static create(params: CreateTransferOrderParams): TransferOrder { + const now = new Date(); + const transferOrderNo = TransferOrder.generateTransferOrderNo(); + + const order = new TransferOrder({ + id: null, + transferOrderNo, + sellerUserId: params.sellerUserId, + sellerAccountSequence: params.sellerAccountSequence, + buyerUserId: params.buyerUserId, + buyerAccountSequence: params.buyerAccountSequence, + sourceOrderNo: params.sourceOrderNo, + sourceAdoptionId: params.sourceAdoptionId, + treeCount: params.treeCount, + contributionPerTree: params.contributionPerTree, + originalAdoptionDate: params.originalAdoptionDate, + originalExpireDate: params.originalExpireDate, + selectedProvince: params.selectedProvince, + selectedCity: params.selectedCity, + transferPrice: params.transferPrice, + platformFeeRate: params.platformFeeRate, + platformFeeAmount: params.platformFeeAmount, + sellerReceiveAmount: params.sellerReceiveAmount, + status: TransferOrderStatus.PENDING, + sagaStep: SagaStep.INIT, + failReason: null, + retryCount: 0, + sellerConfirmedAt: null, + paymentFrozenAt: null, + treesLockedAt: null, + ownershipTransferredAt: null, + contributionAdjustedAt: null, + statsUpdatedAt: null, + paymentSettledAt: null, + completedAt: null, + cancelledAt: null, + rolledBackAt: null, + createdAt: now, + updatedAt: now, + }); + + order._domainEvents.push( + new TransferOrderCreatedEvent(transferOrderNo, { + transferOrderNo, + sellerUserId: params.sellerUserId.toString(), + sellerAccountSequence: params.sellerAccountSequence, + buyerUserId: params.buyerUserId.toString(), + buyerAccountSequence: params.buyerAccountSequence, + sourceOrderNo: params.sourceOrderNo, + treeCount: params.treeCount, + transferPrice: params.transferPrice, + }), + ); + + return order; + } + + /** + * 从数据库重建聚合根(不产生事件) + */ + static reconstitute(data: TransferOrderData): TransferOrder { + return new TransferOrder({ + id: data.id, + transferOrderNo: data.transferOrderNo, + sellerUserId: data.sellerUserId, + sellerAccountSequence: data.sellerAccountSequence, + buyerUserId: data.buyerUserId, + buyerAccountSequence: data.buyerAccountSequence, + sourceOrderNo: data.sourceOrderNo, + sourceAdoptionId: data.sourceAdoptionId, + treeCount: data.treeCount, + contributionPerTree: data.contributionPerTree, + originalAdoptionDate: data.originalAdoptionDate, + originalExpireDate: data.originalExpireDate, + selectedProvince: data.selectedProvince, + selectedCity: data.selectedCity, + transferPrice: data.transferPrice, + platformFeeRate: data.platformFeeRate, + platformFeeAmount: data.platformFeeAmount, + sellerReceiveAmount: data.sellerReceiveAmount, + status: data.status as TransferOrderStatus, + sagaStep: data.sagaStep as SagaStep, + failReason: data.failReason, + retryCount: data.retryCount, + sellerConfirmedAt: data.sellerConfirmedAt, + paymentFrozenAt: data.paymentFrozenAt, + treesLockedAt: data.treesLockedAt, + ownershipTransferredAt: data.ownershipTransferredAt, + contributionAdjustedAt: data.contributionAdjustedAt, + statsUpdatedAt: data.statsUpdatedAt, + paymentSettledAt: data.paymentSettledAt, + completedAt: data.completedAt, + cancelledAt: data.cancelledAt, + rolledBackAt: data.rolledBackAt, + createdAt: data.createdAt, + updatedAt: data.updatedAt, + }); + } + + // ========== 业务方法 ========== + + /** + * 卖方确认转让 + */ + sellerConfirm(): void { + this.assertStatus(TransferOrderStatus.PENDING, '卖方确认'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.SELLER_CONFIRMED; + this._sagaStep = SagaStep.FREEZE_BUYER_PAYMENT; + this._sellerConfirmedAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 买方资金冻结完成 + */ + markPaymentFrozen(): void { + this.assertStatus(TransferOrderStatus.SELLER_CONFIRMED, '冻结资金'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.PAYMENT_FROZEN; + this._sagaStep = SagaStep.LOCK_SELLER_TREES; + this._paymentFrozenAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 卖方树锁定完成 + */ + markTreesLocked(): void { + this.assertStatus(TransferOrderStatus.PAYMENT_FROZEN, '锁定树'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.TREES_LOCKED; + this._sagaStep = SagaStep.TRANSFER_OWNERSHIP; + this._treesLockedAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 所有权变更完成 + */ + markOwnershipTransferred(): void { + this.assertStatus(TransferOrderStatus.TREES_LOCKED, '所有权变更'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.OWNERSHIP_TRANSFERRED; + this._sagaStep = SagaStep.ADJUST_CONTRIBUTION; + this._ownershipTransferredAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 算力调整完成 + */ + markContributionAdjusted(): void { + this.assertStatus(TransferOrderStatus.OWNERSHIP_TRANSFERRED, '算力调整'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.CONTRIBUTION_ADJUSTED; + this._sagaStep = SagaStep.UPDATE_TEAM_STATS; + this._contributionAdjustedAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 团队统计更新完成 + */ + markStatsUpdated(): void { + this.assertStatus(TransferOrderStatus.CONTRIBUTION_ADJUSTED, '团队统计更新'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.STATS_UPDATED; + this._sagaStep = SagaStep.SETTLE_PAYMENT; + this._statsUpdatedAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 资金结算完成 + */ + markPaymentSettled(): void { + this.assertStatus(TransferOrderStatus.STATS_UPDATED, '资金结算'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.PAYMENT_SETTLED; + this._sagaStep = SagaStep.FINALIZE; + this._paymentSettledAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 转让完成 + */ + complete(): void { + this.assertStatus(TransferOrderStatus.PAYMENT_SETTLED, '完成'); + const prev = this.captureState(); + + this._status = TransferOrderStatus.COMPLETED; + this._sagaStep = SagaStep.FINALIZE; + this._completedAt = new Date(); + + this.pushStatusChangedEvent(prev); + + this._domainEvents.push( + new TransferCompletedEvent(this._transferOrderNo, { + transferOrderNo: this._transferOrderNo, + sellerUserId: this._sellerUserId.toString(), + sellerAccountSequence: this._sellerAccountSequence, + buyerUserId: this._buyerUserId.toString(), + buyerAccountSequence: this._buyerAccountSequence, + sourceOrderNo: this._sourceOrderNo, + treeCount: this._treeCount, + contributionPerTree: this._contributionPerTree, + selectedProvince: this._selectedProvince, + selectedCity: this._selectedCity, + originalAdoptionDate: this._originalAdoptionDate.toISOString(), + originalExpireDate: this._originalExpireDate.toISOString(), + transferPrice: this._transferPrice, + completedAt: this._completedAt!.toISOString(), + }), + ); + } + + /** + * 取消转让(仅限 PENDING / SELLER_CONFIRMED 阶段) + */ + cancel(): void { + if (!CANCELLABLE_STATUSES.includes(this._status as any)) { + throw new Error(`当前状态 ${this._status} 不允许取消`); + } + const prev = this.captureState(); + + this._status = TransferOrderStatus.CANCELLED; + this._cancelledAt = new Date(); + + this.pushStatusChangedEvent(prev); + } + + /** + * 标记为失败并开始回滚 + */ + markFailed(reason: string): void { + if (TERMINAL_STATUSES.includes(this._status as any)) { + throw new Error(`当前状态 ${this._status} 已是终态,不能标记为失败`); + } + const prev = this.captureState(); + + this._failReason = reason; + this._status = TransferOrderStatus.ROLLING_BACK; + + // 根据当前步骤确定补偿起点 + if (this._treesLockedAt) { + this._sagaStep = SagaStep.COMPENSATE_UNLOCK_TREES; + } else if (this._paymentFrozenAt) { + this._sagaStep = SagaStep.COMPENSATE_UNFREEZE; + } else { + this._status = TransferOrderStatus.FAILED; + } + + this.pushStatusChangedEvent(prev); + } + + /** + * 补偿完成(解锁树后,继续解冻资金) + */ + advanceCompensation(): void { + if (this._status !== TransferOrderStatus.ROLLING_BACK) { + throw new Error(`当前状态 ${this._status} 不在回滚中`); + } + const prev = this.captureState(); + + if (this._sagaStep === SagaStep.COMPENSATE_UNLOCK_TREES && this._paymentFrozenAt) { + this._sagaStep = SagaStep.COMPENSATE_UNFREEZE; + } else { + this._status = TransferOrderStatus.ROLLED_BACK; + this._rolledBackAt = new Date(); + } + + this.pushStatusChangedEvent(prev); + } + + /** + * 标记回滚完成 + */ + markRolledBack(): void { + const prev = this.captureState(); + this._status = TransferOrderStatus.ROLLED_BACK; + this._rolledBackAt = new Date(); + this.pushStatusChangedEvent(prev); + } + + // ========== Getters ========== + + get id(): bigint | null { return this._id; } + get transferOrderNo(): string { return this._transferOrderNo; } + get sellerUserId(): bigint { return this._sellerUserId; } + get sellerAccountSequence(): string { return this._sellerAccountSequence; } + get buyerUserId(): bigint { return this._buyerUserId; } + get buyerAccountSequence(): string { return this._buyerAccountSequence; } + get sourceOrderNo(): string { return this._sourceOrderNo; } + get sourceAdoptionId(): bigint { return this._sourceAdoptionId; } + get treeCount(): number { return this._treeCount; } + get contributionPerTree(): string { return this._contributionPerTree; } + get originalAdoptionDate(): Date { return this._originalAdoptionDate; } + get originalExpireDate(): Date { return this._originalExpireDate; } + get selectedProvince(): string { return this._selectedProvince; } + get selectedCity(): string { return this._selectedCity; } + get transferPrice(): string { return this._transferPrice; } + get platformFeeRate(): string { return this._platformFeeRate; } + get platformFeeAmount(): string { return this._platformFeeAmount; } + get sellerReceiveAmount(): string { return this._sellerReceiveAmount; } + get status(): TransferOrderStatus { return this._status; } + get sagaStep(): SagaStep { return this._sagaStep; } + get failReason(): string | null { return this._failReason; } + get retryCount(): number { return this._retryCount; } + get sellerConfirmedAt(): Date | null { return this._sellerConfirmedAt; } + get paymentFrozenAt(): Date | null { return this._paymentFrozenAt; } + get treesLockedAt(): Date | null { return this._treesLockedAt; } + get ownershipTransferredAt(): Date | null { return this._ownershipTransferredAt; } + get contributionAdjustedAt(): Date | null { return this._contributionAdjustedAt; } + get statsUpdatedAt(): Date | null { return this._statsUpdatedAt; } + get paymentSettledAt(): Date | null { return this._paymentSettledAt; } + get completedAt(): Date | null { return this._completedAt; } + get cancelledAt(): Date | null { return this._cancelledAt; } + get rolledBackAt(): Date | null { return this._rolledBackAt; } + get createdAt(): Date { return this._createdAt; } + + // ========== 事件管理 ========== + + getDomainEvents(): readonly DomainEvent[] { + return this._domainEvents; + } + + clearDomainEvents(): void { + this._domainEvents = []; + } + + setId(id: bigint): void { + this._id = id; + } + + // ========== 持久化 ========== + + toPersistence(): Omit { + return { + id: this._id, + transferOrderNo: this._transferOrderNo, + sellerUserId: this._sellerUserId, + sellerAccountSequence: this._sellerAccountSequence, + buyerUserId: this._buyerUserId, + buyerAccountSequence: this._buyerAccountSequence, + sourceOrderNo: this._sourceOrderNo, + sourceAdoptionId: this._sourceAdoptionId, + treeCount: this._treeCount, + contributionPerTree: this._contributionPerTree, + originalAdoptionDate: this._originalAdoptionDate, + originalExpireDate: this._originalExpireDate, + selectedProvince: this._selectedProvince, + selectedCity: this._selectedCity, + transferPrice: this._transferPrice, + platformFeeRate: this._platformFeeRate, + platformFeeAmount: this._platformFeeAmount, + sellerReceiveAmount: this._sellerReceiveAmount, + status: this._status, + sagaStep: this._sagaStep, + failReason: this._failReason, + retryCount: this._retryCount, + sellerConfirmedAt: this._sellerConfirmedAt, + paymentFrozenAt: this._paymentFrozenAt, + treesLockedAt: this._treesLockedAt, + ownershipTransferredAt: this._ownershipTransferredAt, + contributionAdjustedAt: this._contributionAdjustedAt, + statsUpdatedAt: this._statsUpdatedAt, + paymentSettledAt: this._paymentSettledAt, + completedAt: this._completedAt, + cancelledAt: this._cancelledAt, + rolledBackAt: this._rolledBackAt, + createdAt: this._createdAt, + }; + } + + // ========== 私有辅助方法 ========== + + private assertStatus(expected: TransferOrderStatus, operation: string): void { + if (this._status !== expected) { + throw new Error(`操作"${operation}"要求状态为 ${expected},当前状态为 ${this._status}`); + } + } + + private captureState(): { status: string; sagaStep: string } { + return { status: this._status, sagaStep: this._sagaStep }; + } + + private pushStatusChangedEvent(prev: { status: string; sagaStep: string }): void { + this._domainEvents.push( + new TransferStatusChangedEvent(this._transferOrderNo, { + transferOrderNo: this._transferOrderNo, + fromStatus: prev.status, + toStatus: this._status, + fromSagaStep: prev.sagaStep, + toSagaStep: this._sagaStep, + }), + ); + } +} diff --git a/backend/services/transfer-service/src/domain/domain.module.ts b/backend/services/transfer-service/src/domain/domain.module.ts new file mode 100644 index 00000000..bf7d17e6 --- /dev/null +++ b/backend/services/transfer-service/src/domain/domain.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TransferFeeDomainService } from './services/transfer-fee.service'; + +@Module({ + providers: [TransferFeeDomainService], + exports: [TransferFeeDomainService], +}) +export class DomainModule {} diff --git a/backend/services/transfer-service/src/domain/events/domain-event.interface.ts b/backend/services/transfer-service/src/domain/events/domain-event.interface.ts new file mode 100644 index 00000000..dec33b42 --- /dev/null +++ b/backend/services/transfer-service/src/domain/events/domain-event.interface.ts @@ -0,0 +1,7 @@ +export interface DomainEvent { + type: string; + aggregateId: string; + aggregateType: string; + occurredAt: Date; + data: Record; +} diff --git a/backend/services/transfer-service/src/domain/events/index.ts b/backend/services/transfer-service/src/domain/events/index.ts new file mode 100644 index 00000000..05a6cfbc --- /dev/null +++ b/backend/services/transfer-service/src/domain/events/index.ts @@ -0,0 +1,4 @@ +export * from './domain-event.interface'; +export * from './transfer-order-created.event'; +export * from './transfer-status-changed.event'; +export * from './transfer-completed.event'; diff --git a/backend/services/transfer-service/src/domain/events/transfer-completed.event.ts b/backend/services/transfer-service/src/domain/events/transfer-completed.event.ts new file mode 100644 index 00000000..6b332455 --- /dev/null +++ b/backend/services/transfer-service/src/domain/events/transfer-completed.event.ts @@ -0,0 +1,29 @@ +import { DomainEvent } from './domain-event.interface'; + +export class TransferCompletedEvent implements DomainEvent { + readonly type = 'TransferCompleted'; + readonly aggregateType = 'TransferOrder'; + readonly occurredAt: Date; + + constructor( + public readonly aggregateId: string, + public readonly data: { + transferOrderNo: string; + sellerUserId: string; + sellerAccountSequence: string; + buyerUserId: string; + buyerAccountSequence: string; + sourceOrderNo: string; + treeCount: number; + contributionPerTree: string; + selectedProvince: string; + selectedCity: string; + originalAdoptionDate: string; + originalExpireDate: string; + transferPrice: string; + completedAt: string; + }, + ) { + this.occurredAt = new Date(); + } +} diff --git a/backend/services/transfer-service/src/domain/events/transfer-order-created.event.ts b/backend/services/transfer-service/src/domain/events/transfer-order-created.event.ts new file mode 100644 index 00000000..2ca9a437 --- /dev/null +++ b/backend/services/transfer-service/src/domain/events/transfer-order-created.event.ts @@ -0,0 +1,23 @@ +import { DomainEvent } from './domain-event.interface'; + +export class TransferOrderCreatedEvent implements DomainEvent { + readonly type = 'TransferOrderCreated'; + readonly aggregateType = 'TransferOrder'; + readonly occurredAt: Date; + + constructor( + public readonly aggregateId: string, + public readonly data: { + transferOrderNo: string; + sellerUserId: string; + sellerAccountSequence: string; + buyerUserId: string; + buyerAccountSequence: string; + sourceOrderNo: string; + treeCount: number; + transferPrice: string; + }, + ) { + this.occurredAt = new Date(); + } +} diff --git a/backend/services/transfer-service/src/domain/events/transfer-status-changed.event.ts b/backend/services/transfer-service/src/domain/events/transfer-status-changed.event.ts new file mode 100644 index 00000000..2c83fe69 --- /dev/null +++ b/backend/services/transfer-service/src/domain/events/transfer-status-changed.event.ts @@ -0,0 +1,20 @@ +import { DomainEvent } from './domain-event.interface'; + +export class TransferStatusChangedEvent implements DomainEvent { + readonly type = 'TransferStatusChanged'; + readonly aggregateType = 'TransferOrder'; + readonly occurredAt: Date; + + constructor( + public readonly aggregateId: string, + public readonly data: { + transferOrderNo: string; + fromStatus: string; + toStatus: string; + fromSagaStep: string; + toSagaStep: string; + }, + ) { + this.occurredAt = new Date(); + } +} diff --git a/backend/services/transfer-service/src/domain/index.ts b/backend/services/transfer-service/src/domain/index.ts new file mode 100644 index 00000000..22e2e6e5 --- /dev/null +++ b/backend/services/transfer-service/src/domain/index.ts @@ -0,0 +1,5 @@ +export * from './aggregates/transfer-order.aggregate'; +export * from './events'; +export * from './value-objects'; +export * from './repositories/transfer-order.repository.interface'; +export * from './services/transfer-fee.service'; diff --git a/backend/services/transfer-service/src/domain/repositories/transfer-order.repository.interface.ts b/backend/services/transfer-service/src/domain/repositories/transfer-order.repository.interface.ts new file mode 100644 index 00000000..d253ce40 --- /dev/null +++ b/backend/services/transfer-service/src/domain/repositories/transfer-order.repository.interface.ts @@ -0,0 +1,13 @@ +import { TransferOrder } from '../aggregates/transfer-order.aggregate'; + +export interface ITransferOrderRepository { + findById(id: bigint): Promise; + findByTransferOrderNo(transferOrderNo: string): Promise; + findBySellerUserId(userId: bigint, options?: { limit?: number; offset?: number }): Promise; + findByBuyerUserId(userId: bigint, options?: { limit?: number; offset?: number }): Promise; + findBySourceOrderNo(sourceOrderNo: string): Promise; + findByStatus(status: string, limit?: number): Promise; + countByUserAsSellerOrBuyer(userId: bigint): Promise; +} + +export const TRANSFER_ORDER_REPOSITORY = Symbol('ITransferOrderRepository'); diff --git a/backend/services/transfer-service/src/domain/services/transfer-fee.service.ts b/backend/services/transfer-service/src/domain/services/transfer-fee.service.ts new file mode 100644 index 00000000..68232327 --- /dev/null +++ b/backend/services/transfer-service/src/domain/services/transfer-fee.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; + +/** + * 转让费用计算领域服务 + */ +@Injectable() +export class TransferFeeDomainService { + /** + * 平台手续费率:2% + */ + private readonly PLATFORM_FEE_RATE = '0.0200'; + + /** + * 计算转让费用 + */ + calculateFees(transferPrice: string): { + platformFeeRate: string; + platformFeeAmount: string; + sellerReceiveAmount: string; + } { + const price = parseFloat(transferPrice); + const feeRate = parseFloat(this.PLATFORM_FEE_RATE); + const feeAmount = Math.round(price * feeRate * 100000000) / 100000000; + const sellerReceive = Math.round((price - feeAmount) * 100000000) / 100000000; + + return { + platformFeeRate: this.PLATFORM_FEE_RATE, + platformFeeAmount: feeAmount.toFixed(8), + sellerReceiveAmount: sellerReceive.toFixed(8), + }; + } +} diff --git a/backend/services/transfer-service/src/domain/value-objects/index.ts b/backend/services/transfer-service/src/domain/value-objects/index.ts new file mode 100644 index 00000000..73d68778 --- /dev/null +++ b/backend/services/transfer-service/src/domain/value-objects/index.ts @@ -0,0 +1,2 @@ +export * from './transfer-order-status.enum'; +export * from './saga-step.enum'; diff --git a/backend/services/transfer-service/src/domain/value-objects/saga-step.enum.ts b/backend/services/transfer-service/src/domain/value-objects/saga-step.enum.ts new file mode 100644 index 00000000..a78f0946 --- /dev/null +++ b/backend/services/transfer-service/src/domain/value-objects/saga-step.enum.ts @@ -0,0 +1,14 @@ +export enum SagaStep { + INIT = 'INIT', // 初始化 + FREEZE_BUYER_PAYMENT = 'FREEZE_BUYER_PAYMENT', // 冻结买方资金 + LOCK_SELLER_TREES = 'LOCK_SELLER_TREES', // 锁定卖方树 + TRANSFER_OWNERSHIP = 'TRANSFER_OWNERSHIP', // 变更所有权 + ADJUST_CONTRIBUTION = 'ADJUST_CONTRIBUTION', // 调整算力 + UPDATE_TEAM_STATS = 'UPDATE_TEAM_STATS', // 更新团队统计 + SETTLE_PAYMENT = 'SETTLE_PAYMENT', // 结算资金 + FINALIZE = 'FINALIZE', // 完成 + + // 补偿步骤 + COMPENSATE_UNLOCK_TREES = 'COMPENSATE_UNLOCK_TREES', // 补偿:解锁树 + COMPENSATE_UNFREEZE = 'COMPENSATE_UNFREEZE', // 补偿:解冻资金 +} diff --git a/backend/services/transfer-service/src/domain/value-objects/transfer-order-status.enum.ts b/backend/services/transfer-service/src/domain/value-objects/transfer-order-status.enum.ts new file mode 100644 index 00000000..ef916486 --- /dev/null +++ b/backend/services/transfer-service/src/domain/value-objects/transfer-order-status.enum.ts @@ -0,0 +1,33 @@ +export enum TransferOrderStatus { + PENDING = 'PENDING', // 待卖方确认 + SELLER_CONFIRMED = 'SELLER_CONFIRMED', // 卖方已确认 + PAYMENT_FROZEN = 'PAYMENT_FROZEN', // 买方资金已冻结 + TREES_LOCKED = 'TREES_LOCKED', // 卖方树已锁定 + OWNERSHIP_TRANSFERRED = 'OWNERSHIP_TRANSFERRED', // 所有权已变更 + CONTRIBUTION_ADJUSTED = 'CONTRIBUTION_ADJUSTED', // 算力已调整 + STATS_UPDATED = 'STATS_UPDATED', // 团队统计已更新 + PAYMENT_SETTLED = 'PAYMENT_SETTLED', // 资金已结算 + COMPLETED = 'COMPLETED', // 转让完成 + + CANCELLED = 'CANCELLED', // 已取消 + FAILED = 'FAILED', // 失败 + ROLLING_BACK = 'ROLLING_BACK', // 补偿中 + ROLLED_BACK = 'ROLLED_BACK', // 已回滚 +} + +/** + * 可取消的状态(PENDING 和 SELLER_CONFIRMED 阶段可取消) + */ +export const CANCELLABLE_STATUSES = [ + TransferOrderStatus.PENDING, + TransferOrderStatus.SELLER_CONFIRMED, +] as const; + +/** + * 终态(不可再变更) + */ +export const TERMINAL_STATUSES = [ + TransferOrderStatus.COMPLETED, + TransferOrderStatus.CANCELLED, + TransferOrderStatus.ROLLED_BACK, +] as const; diff --git a/backend/services/transfer-service/src/infrastructure/external/identity-service.client.ts b/backend/services/transfer-service/src/infrastructure/external/identity-service.client.ts new file mode 100644 index 00000000..d90c5c63 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/external/identity-service.client.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +/** + * Identity Service 客户端 + * 验证用户身份和实名认证状态 + */ +@Injectable() +export class IdentityServiceClient { + private readonly logger = new Logger(IdentityServiceClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'external.identityServiceUrl', + 'http://localhost:3000', + ); + } + + /** + * 检查用户是否已实名认证 + */ + async isKycVerified(userId: string): Promise { + const url = `${this.baseUrl}/api/v1/internal/users/${userId}/kyc-status`; + try { + const { data } = await firstValueFrom(this.httpService.get(url)); + return data.verified === true; + } catch { + return false; + } + } + + /** + * 获取用户信息 + */ + async getUserInfo(userId: string): Promise<{ + userId: number; + accountSequence: string; + kycVerified: boolean; + } | null> { + const url = `${this.baseUrl}/api/v1/internal/users/${userId}`; + try { + const { data } = await firstValueFrom(this.httpService.get(url)); + return data; + } catch (error: any) { + if (error?.response?.status === 404) return null; + throw error; + } + } +} diff --git a/backend/services/transfer-service/src/infrastructure/external/index.ts b/backend/services/transfer-service/src/infrastructure/external/index.ts new file mode 100644 index 00000000..eade5ce1 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/external/index.ts @@ -0,0 +1,3 @@ +export * from './wallet-service.client'; +export * from './planting-service.client'; +export * from './identity-service.client'; diff --git a/backend/services/transfer-service/src/infrastructure/external/planting-service.client.ts b/backend/services/transfer-service/src/infrastructure/external/planting-service.client.ts new file mode 100644 index 00000000..ff9ee354 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/external/planting-service.client.ts @@ -0,0 +1,83 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +/** + * Planting Service 客户端 + * 查询认种订单和持仓信息 + */ +@Injectable() +export class PlantingServiceClient { + private readonly logger = new Logger(PlantingServiceClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'external.plantingServiceUrl', + 'http://localhost:3003', + ); + } + + /** + * 查询认种订单详情(包含可转让信息) + */ + async getPlantingOrder(orderNo: string): Promise<{ + id: number; + orderNo: string; + userId: number; + accountSequence: string; + treeCount: number; + status: string; + selectedProvince: string; + selectedCity: string; + transferLockedCount: number; + transferredCount: number; + availableForTransfer: number; + miningEnabledAt: string; + createdAt: string; + } | null> { + const url = `${this.baseUrl}/api/v1/internal/planting-orders/${orderNo}`; + try { + const { data } = await firstValueFrom(this.httpService.get(url)); + return data; + } catch (error: any) { + if (error?.response?.status === 404) return null; + throw error; + } + } + + /** + * 查询用户持仓 + */ + async getUserPosition(userId: string): Promise<{ + userId: number; + totalTreeCount: number; + effectiveTreeCount: number; + } | null> { + const url = `${this.baseUrl}/api/v1/internal/planting-positions/${userId}`; + try { + const { data } = await firstValueFrom(this.httpService.get(url)); + return data; + } catch (error: any) { + if (error?.response?.status === 404) return null; + throw error; + } + } + + /** + * 查询树的原始算力值 + */ + async getContributionPerTree(orderNo: string): Promise<{ + contributionPerTree: string; + adoptionDate: string; + expireDate: string; + }> { + const url = `${this.baseUrl}/api/v1/internal/planting-orders/${orderNo}/contribution`; + const { data } = await firstValueFrom(this.httpService.get(url)); + return data; + } +} diff --git a/backend/services/transfer-service/src/infrastructure/external/wallet-service.client.ts b/backend/services/transfer-service/src/infrastructure/external/wallet-service.client.ts new file mode 100644 index 00000000..03c3a52b --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/external/wallet-service.client.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +/** + * Wallet Service 客户端 + * 跨服务调用 wallet-service 的冻结/解冻/转账接口 + */ +@Injectable() +export class WalletServiceClient { + private readonly logger = new Logger(WalletServiceClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'external.walletServiceUrl', + 'http://localhost:3001', + ); + } + + /** + * 冻结买方资金 + */ + async freezePayment(params: { + accountSequence: string; + amount: string; + transferOrderNo: string; + reason: string; + }): Promise<{ freezeId: string }> { + const url = `${this.baseUrl}/api/v1/internal/wallet/freeze`; + this.logger.log(`[WALLET] Freezing ${params.amount} USDT for ${params.accountSequence}`); + + const { data } = await firstValueFrom( + this.httpService.post(url, params), + ); + return data; + } + + /** + * 解冻买方资金(补偿操作) + */ + async unfreezePayment(params: { + accountSequence: string; + freezeId: string; + transferOrderNo: string; + reason: string; + }): Promise { + const url = `${this.baseUrl}/api/v1/internal/wallet/unfreeze`; + this.logger.log(`[WALLET] Unfreezing for ${params.accountSequence}`); + + await firstValueFrom(this.httpService.post(url, params)); + } + + /** + * 结算转让资金 + * 从买方冻结金额 → 卖方实收 + 平台手续费 + */ + async settleTransferPayment(params: { + buyerAccountSequence: string; + sellerAccountSequence: string; + freezeId: string; + sellerReceiveAmount: string; + platformFeeAmount: string; + transferOrderNo: string; + }): Promise { + const url = `${this.baseUrl}/api/v1/internal/wallet/settle-transfer`; + this.logger.log(`[WALLET] Settling transfer ${params.transferOrderNo}`); + + await firstValueFrom(this.httpService.post(url, params)); + } + + /** + * 查询账户余额 + */ + async getBalance(accountSequence: string): Promise<{ available: string; frozen: string }> { + const url = `${this.baseUrl}/api/v1/internal/wallet/balance/${accountSequence}`; + const { data } = await firstValueFrom(this.httpService.get(url)); + return data; + } +} diff --git a/backend/services/transfer-service/src/infrastructure/index.ts b/backend/services/transfer-service/src/infrastructure/index.ts new file mode 100644 index 00000000..7b4cee4e --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/index.ts @@ -0,0 +1 @@ +export * from './infrastructure.module'; diff --git a/backend/services/transfer-service/src/infrastructure/infrastructure.module.ts b/backend/services/transfer-service/src/infrastructure/infrastructure.module.ts new file mode 100644 index 00000000..c5696dd6 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/infrastructure.module.ts @@ -0,0 +1,53 @@ +import { Module, Global } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { PrismaService } from './persistence/prisma/prisma.service'; +import { TransferOrderRepositoryImpl } from './persistence/repositories/transfer-order.repository.impl'; +import { OutboxRepository } from './persistence/repositories/outbox.repository'; +import { UnitOfWork, UNIT_OF_WORK } from './persistence/unit-of-work'; +import { WalletServiceClient } from './external/wallet-service.client'; +import { PlantingServiceClient } from './external/planting-service.client'; +import { IdentityServiceClient } from './external/identity-service.client'; +import { KafkaModule } from './kafka/kafka.module'; +import { OutboxPublisherService } from './kafka/outbox-publisher.service'; +import { EventAckController } from './kafka/event-ack.controller'; +import { SagaEventConsumer } from './kafka/saga-event.consumer'; +import { TRANSFER_ORDER_REPOSITORY } from '../domain/repositories/transfer-order.repository.interface'; + +@Global() +@Module({ + imports: [ + HttpModule.register({ + timeout: 5000, + maxRedirects: 5, + }), + KafkaModule, + ], + controllers: [EventAckController, SagaEventConsumer], + providers: [ + PrismaService, + { + provide: TRANSFER_ORDER_REPOSITORY, + useClass: TransferOrderRepositoryImpl, + }, + { + provide: UNIT_OF_WORK, + useClass: UnitOfWork, + }, + OutboxRepository, + OutboxPublisherService, + WalletServiceClient, + PlantingServiceClient, + IdentityServiceClient, + ], + exports: [ + PrismaService, + TRANSFER_ORDER_REPOSITORY, + UNIT_OF_WORK, + OutboxRepository, + OutboxPublisherService, + WalletServiceClient, + PlantingServiceClient, + IdentityServiceClient, + ], +}) +export class InfrastructureModule {} diff --git a/backend/services/transfer-service/src/infrastructure/kafka/event-ack.controller.ts b/backend/services/transfer-service/src/infrastructure/kafka/event-ack.controller.ts new file mode 100644 index 00000000..84fc5020 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/event-ack.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Logger } from '@nestjs/common'; +import { MessagePattern, Payload } from '@nestjs/microservices'; +import { OutboxRepository } from '../persistence/repositories/outbox.repository'; + +/** + * 事件确认控制器 + * 接收来自其他服务的事件消费确认 + */ +@Controller() +export class EventAckController { + private readonly logger = new Logger(EventAckController.name); + + constructor(private readonly outboxRepository: OutboxRepository) {} + + @MessagePattern('transfer.events.ack') + async handleEventAck( + @Payload() message: { eventId: string; eventType?: string; status: string }, + ): Promise { + this.logger.log(`[ACK] Received ack for event ${message.eventId} (${message.eventType || 'any'})`); + + if (message.status === 'SUCCESS') { + await this.outboxRepository.markAsConfirmed(message.eventId, message.eventType); + } else { + this.logger.warn(`[ACK] Event ${message.eventId} reported failure: ${message.status}`); + } + } +} diff --git a/backend/services/transfer-service/src/infrastructure/kafka/event-publisher.service.ts b/backend/services/transfer-service/src/infrastructure/kafka/event-publisher.service.ts new file mode 100644 index 00000000..4227c9da --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/event-publisher.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger, Inject, OnModuleInit } from '@nestjs/common'; +import { ClientKafka } from '@nestjs/microservices'; + +/** + * Kafka 事件发布服务 + * 直接发布事件到 Kafka(用于非 Outbox 场景) + */ +@Injectable() +export class EventPublisherService implements OnModuleInit { + private readonly logger = new Logger(EventPublisherService.name); + + constructor( + @Inject('KAFKA_SERVICE') + private readonly kafkaClient: ClientKafka, + ) {} + + async onModuleInit() { + await this.kafkaClient.connect(); + this.logger.log('Kafka EventPublisher connected'); + } + + /** + * 发布事件到指定 topic + */ + async publish(topic: string, key: string, payload: Record): Promise { + try { + this.kafkaClient.emit(topic, { + key, + value: JSON.stringify(payload), + }); + this.logger.debug(`Published event to ${topic} with key ${key}`); + } catch (error) { + this.logger.error(`Failed to publish to ${topic}: ${error}`); + throw error; + } + } +} diff --git a/backend/services/transfer-service/src/infrastructure/kafka/index.ts b/backend/services/transfer-service/src/infrastructure/kafka/index.ts new file mode 100644 index 00000000..0335eae6 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/index.ts @@ -0,0 +1,5 @@ +export * from './kafka.module'; +export * from './event-publisher.service'; +export * from './outbox-publisher.service'; +export * from './event-ack.controller'; +export * from './saga-event.consumer'; diff --git a/backend/services/transfer-service/src/infrastructure/kafka/kafka.module.ts b/backend/services/transfer-service/src/infrastructure/kafka/kafka.module.ts new file mode 100644 index 00000000..891ad417 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/kafka.module.ts @@ -0,0 +1,32 @@ +import { Module, Global } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { EventPublisherService } from './event-publisher.service'; + +@Global() +@Module({ + imports: [ + ClientsModule.registerAsync([ + { + name: 'KAFKA_SERVICE', + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + transport: Transport.KAFKA, + options: { + client: { + clientId: configService.get('KAFKA_CLIENT_ID', 'transfer-service'), + brokers: configService.get('KAFKA_BROKERS', 'localhost:9092').split(','), + }, + producer: { + allowAutoTopicCreation: true, + }, + }, + }), + inject: [ConfigService], + }, + ]), + ], + providers: [EventPublisherService], + exports: [EventPublisherService, ClientsModule], +}) +export class KafkaModule {} diff --git a/backend/services/transfer-service/src/infrastructure/kafka/outbox-publisher.service.ts b/backend/services/transfer-service/src/infrastructure/kafka/outbox-publisher.service.ts new file mode 100644 index 00000000..02f4f828 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/outbox-publisher.service.ts @@ -0,0 +1,156 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ClientKafka } from '@nestjs/microservices'; +import { OutboxRepository, OutboxEvent } from '../persistence/repositories/outbox.repository'; + +/** + * Outbox Publisher Service (B方案 - 消费方确认模式) + */ +@Injectable() +export class OutboxPublisherService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(OutboxPublisherService.name); + private isRunning = false; + private pollInterval: NodeJS.Timeout | null = null; + private timeoutCheckInterval: NodeJS.Timeout | null = null; + private cleanupInterval: NodeJS.Timeout | null = null; + private isConnected = false; + + private readonly pollIntervalMs: number; + private readonly batchSize: number; + private readonly cleanupIntervalMs: number; + private readonly confirmationTimeoutMinutes: number; + private readonly timeoutCheckIntervalMs: number; + + constructor( + @Inject('KAFKA_SERVICE') + private readonly kafkaClient: ClientKafka, + private readonly outboxRepository: OutboxRepository, + private readonly configService: ConfigService, + ) { + this.pollIntervalMs = this.configService.get('OUTBOX_POLL_INTERVAL_MS', 1000); + this.batchSize = this.configService.get('OUTBOX_BATCH_SIZE', 100); + this.cleanupIntervalMs = this.configService.get('OUTBOX_CLEANUP_INTERVAL_MS', 3600000); + this.confirmationTimeoutMinutes = this.configService.get('OUTBOX_CONFIRMATION_TIMEOUT_MINUTES', 5); + this.timeoutCheckIntervalMs = this.configService.get('OUTBOX_TIMEOUT_CHECK_INTERVAL_MS', 60000); + } + + async onModuleInit() { + try { + await this.kafkaClient.connect(); + this.isConnected = true; + this.logger.log('[OUTBOX] Connected to Kafka'); + this.start(); + } catch (error) { + this.logger.error('[OUTBOX] Failed to connect to Kafka:', error); + } + } + + async onModuleDestroy() { + this.stop(); + if (this.isConnected) { + await this.kafkaClient.close(); + } + } + + start(): void { + if (this.isRunning) return; + this.isRunning = true; + + this.pollInterval = setInterval(() => { + this.processOutbox().catch((err) => { + this.logger.error('[OUTBOX] Error processing outbox:', err); + }); + }, this.pollIntervalMs); + + this.timeoutCheckInterval = setInterval(() => { + this.checkConfirmationTimeouts().catch((err) => { + this.logger.error('[OUTBOX] Error checking timeouts:', err); + }); + }, this.timeoutCheckIntervalMs); + + this.cleanupInterval = setInterval(() => { + this.cleanup().catch((err) => { + this.logger.error('[OUTBOX] Error cleaning up:', err); + }); + }, this.cleanupIntervalMs); + + this.logger.log('[OUTBOX] Outbox publisher started (B方案)'); + } + + stop(): void { + if (!this.isRunning) return; + this.isRunning = false; + + if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } + if (this.timeoutCheckInterval) { clearInterval(this.timeoutCheckInterval); this.timeoutCheckInterval = null; } + if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } + } + + async processOutbox(): Promise { + if (!this.isConnected) return; + + try { + const pendingEvents = await this.outboxRepository.findPendingEvents(this.batchSize); + const retryEvents = await this.outboxRepository.findEventsForRetry(Math.floor(this.batchSize / 2)); + const allEvents = [...pendingEvents, ...retryEvents]; + + if (allEvents.length === 0) return; + + for (const event of allEvents) { + await this.publishEvent(event); + } + } catch (error) { + this.logger.error('[OUTBOX] Error in processOutbox:', error); + } + } + + private async publishEvent(event: OutboxEvent): Promise { + try { + const payload = { + ...(event.payload as Record), + _outbox: { + id: event.id.toString(), + aggregateId: event.aggregateId, + eventType: event.eventType, + }, + }; + + this.kafkaClient.emit(event.topic, { + key: event.key, + value: JSON.stringify(payload), + }); + + await this.outboxRepository.markAsSent(event.id); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await this.outboxRepository.markAsFailed(event.id, errorMessage); + } + } + + private async checkConfirmationTimeouts(): Promise { + if (!this.isConnected) return; + + try { + const timedOutEvents = await this.outboxRepository.findSentEventsTimedOut( + this.confirmationTimeoutMinutes, + this.batchSize, + ); + + for (const event of timedOutEvents) { + await this.outboxRepository.resetSentToPending(event.id); + } + } catch (error) { + this.logger.error('[OUTBOX] Error checking timeouts:', error); + } + } + + private async cleanup(): Promise { + const retentionDays = this.configService.get('OUTBOX_RETENTION_DAYS', 7); + await this.outboxRepository.cleanupOldEvents(retentionDays); + } + + async getStats() { + const stats = await this.outboxRepository.getStats(); + return { isRunning: this.isRunning, isConnected: this.isConnected, ...stats }; + } +} diff --git a/backend/services/transfer-service/src/infrastructure/kafka/saga-event.consumer.ts b/backend/services/transfer-service/src/infrastructure/kafka/saga-event.consumer.ts new file mode 100644 index 00000000..993028e2 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/kafka/saga-event.consumer.ts @@ -0,0 +1,53 @@ +import { Controller, Logger } from '@nestjs/common'; +import { MessagePattern, Payload } from '@nestjs/microservices'; + +/** + * Saga 事件消费者 + * 接收来自各服务的 Saga 步骤完成/失败确认 + * 具体的 Saga 推进逻辑将在 Application 层的 SagaOrchestrator 中实现 + */ +@Controller() +export class SagaEventConsumer { + private readonly logger = new Logger(SagaEventConsumer.name); + + /** + * planting-service: 树锁定完成确认 + */ + @MessagePattern('transfer.trees.lock.ack') + async handleTreesLockAck(@Payload() message: any): Promise { + this.logger.log(`[SAGA] Trees lock ack received: ${JSON.stringify(message)}`); + // 将在 SagaOrchestrator 注入后委托处理 + } + + /** + * planting-service: 所有权变更完成确认 + */ + @MessagePattern('planting.transfer.completed') + async handleOwnershipTransferCompleted(@Payload() message: any): Promise { + this.logger.log(`[SAGA] Ownership transfer completed: ${JSON.stringify(message)}`); + } + + /** + * contribution-service: 算力调整完成确认 + */ + @MessagePattern('contribution.transfer.adjusted') + async handleContributionAdjusted(@Payload() message: any): Promise { + this.logger.log(`[SAGA] Contribution adjusted: ${JSON.stringify(message)}`); + } + + /** + * referral-service: 团队统计更新完成确认 + */ + @MessagePattern('referral.transfer.stats-updated') + async handleStatsUpdated(@Payload() message: any): Promise { + this.logger.log(`[SAGA] Team stats updated: ${JSON.stringify(message)}`); + } + + /** + * planting-service: 树解锁完成确认(补偿) + */ + @MessagePattern('transfer.trees.unlock.ack') + async handleTreesUnlockAck(@Payload() message: any): Promise { + this.logger.log(`[SAGA] Trees unlock ack received: ${JSON.stringify(message)}`); + } +} diff --git a/backend/services/transfer-service/src/infrastructure/persistence/mappers/transfer-order.mapper.ts b/backend/services/transfer-service/src/infrastructure/persistence/mappers/transfer-order.mapper.ts new file mode 100644 index 00000000..139a5ee5 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/persistence/mappers/transfer-order.mapper.ts @@ -0,0 +1,89 @@ +import { TransferOrder, TransferOrderData } from '../../../domain/aggregates/transfer-order.aggregate'; + +/** + * TransferOrder Domain <-> Persistence 映射器 + */ +export class TransferOrderMapper { + /** + * Prisma 模型 → Domain 聚合根 + */ + static toDomain(prismaOrder: any): TransferOrder { + const data: TransferOrderData = { + id: prismaOrder.id, + transferOrderNo: prismaOrder.transferOrderNo, + sellerUserId: prismaOrder.sellerUserId, + sellerAccountSequence: prismaOrder.sellerAccountSequence, + buyerUserId: prismaOrder.buyerUserId, + buyerAccountSequence: prismaOrder.buyerAccountSequence, + sourceOrderNo: prismaOrder.sourceOrderNo, + sourceAdoptionId: prismaOrder.sourceAdoptionId, + treeCount: prismaOrder.treeCount, + contributionPerTree: prismaOrder.contributionPerTree.toString(), + originalAdoptionDate: prismaOrder.originalAdoptionDate, + originalExpireDate: prismaOrder.originalExpireDate, + selectedProvince: prismaOrder.selectedProvince, + selectedCity: prismaOrder.selectedCity, + transferPrice: prismaOrder.transferPrice.toString(), + platformFeeRate: prismaOrder.platformFeeRate.toString(), + platformFeeAmount: prismaOrder.platformFeeAmount.toString(), + sellerReceiveAmount: prismaOrder.sellerReceiveAmount.toString(), + status: prismaOrder.status, + sagaStep: prismaOrder.sagaStep, + failReason: prismaOrder.failReason, + retryCount: prismaOrder.retryCount, + sellerConfirmedAt: prismaOrder.sellerConfirmedAt, + paymentFrozenAt: prismaOrder.paymentFrozenAt, + treesLockedAt: prismaOrder.treesLockedAt, + ownershipTransferredAt: prismaOrder.ownershipTransferredAt, + contributionAdjustedAt: prismaOrder.contributionAdjustedAt, + statsUpdatedAt: prismaOrder.statsUpdatedAt, + paymentSettledAt: prismaOrder.paymentSettledAt, + completedAt: prismaOrder.completedAt, + cancelledAt: prismaOrder.cancelledAt, + rolledBackAt: prismaOrder.rolledBackAt, + createdAt: prismaOrder.createdAt, + updatedAt: prismaOrder.updatedAt, + }; + + return TransferOrder.reconstitute(data); + } + + /** + * Domain 聚合根 → Prisma 数据 + */ + static toPersistence(order: TransferOrder) { + return { + transferOrderNo: order.transferOrderNo, + sellerUserId: order.sellerUserId, + sellerAccountSequence: order.sellerAccountSequence, + buyerUserId: order.buyerUserId, + buyerAccountSequence: order.buyerAccountSequence, + sourceOrderNo: order.sourceOrderNo, + sourceAdoptionId: order.sourceAdoptionId, + treeCount: order.treeCount, + contributionPerTree: order.contributionPerTree, + originalAdoptionDate: order.originalAdoptionDate, + originalExpireDate: order.originalExpireDate, + selectedProvince: order.selectedProvince, + selectedCity: order.selectedCity, + transferPrice: order.transferPrice, + platformFeeRate: order.platformFeeRate, + platformFeeAmount: order.platformFeeAmount, + sellerReceiveAmount: order.sellerReceiveAmount, + status: order.status, + sagaStep: order.sagaStep, + failReason: order.failReason, + retryCount: order.retryCount, + sellerConfirmedAt: order.sellerConfirmedAt, + paymentFrozenAt: order.paymentFrozenAt, + treesLockedAt: order.treesLockedAt, + ownershipTransferredAt: order.ownershipTransferredAt, + contributionAdjustedAt: order.contributionAdjustedAt, + statsUpdatedAt: order.statsUpdatedAt, + paymentSettledAt: order.paymentSettledAt, + completedAt: order.completedAt, + cancelledAt: order.cancelledAt, + rolledBackAt: order.rolledBackAt, + }; + } +} diff --git a/backend/services/transfer-service/src/infrastructure/persistence/prisma/prisma.service.ts b/backend/services/transfer-service/src/infrastructure/persistence/prisma/prisma.service.ts new file mode 100644 index 00000000..014beb1f --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/persistence/prisma/prisma.service.ts @@ -0,0 +1,67 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient, Prisma } from '@prisma/client'; + +// 定义事务客户端类型 +export type TransactionClient = Omit< + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + constructor() { + super({ + log: + process.env.NODE_ENV === 'development' + ? ['query', 'info', 'warn', 'error'] + : ['error'], + }); + } + + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } + + /** + * 执行数据库事务 + */ + async executeTransaction( + fn: (tx: TransactionClient) => Promise, + options?: { + maxWait?: number; + timeout?: number; + isolationLevel?: Prisma.TransactionIsolationLevel; + }, + ): Promise { + return this.$transaction(fn, { + maxWait: options?.maxWait ?? 5000, + timeout: options?.timeout ?? 10000, + isolationLevel: options?.isolationLevel ?? Prisma.TransactionIsolationLevel.ReadCommitted, + }); + } + + async cleanDatabase() { + if (process.env.NODE_ENV !== 'test') { + throw new Error('cleanDatabase can only be used in test environment'); + } + + const tablenames = await this.$queryRaw< + Array<{ tablename: string }> + >`SELECT tablename FROM pg_tables WHERE schemaname='public'`; + + for (const { tablename } of tablenames) { + if (tablename !== '_prisma_migrations') { + await this.$executeRawUnsafe( + `TRUNCATE TABLE "public"."${tablename}" CASCADE;`, + ); + } + } + } +} diff --git a/backend/services/transfer-service/src/infrastructure/persistence/repositories/outbox.repository.ts b/backend/services/transfer-service/src/infrastructure/persistence/repositories/outbox.repository.ts new file mode 100644 index 00000000..45c85daf --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/persistence/repositories/outbox.repository.ts @@ -0,0 +1,230 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { Prisma } from '@prisma/client'; + +export enum OutboxStatus { + PENDING = 'PENDING', + SENT = 'SENT', + CONFIRMED = 'CONFIRMED', + FAILED = 'FAILED', +} + +export interface OutboxEventData { + eventType: string; + topic: string; + key: string; + payload: Record; + aggregateId: string; + aggregateType: string; +} + +export interface OutboxEvent extends OutboxEventData { + id: bigint; + status: OutboxStatus; + retryCount: number; + maxRetries: number; + lastError: string | null; + createdAt: Date; + publishedAt: Date | null; + nextRetryAt: Date | null; +} + +@Injectable() +export class OutboxRepository { + private readonly logger = new Logger(OutboxRepository.name); + + constructor(private readonly prisma: PrismaService) {} + + async saveInTransaction( + tx: Prisma.TransactionClient, + events: OutboxEventData[], + ): Promise { + if (events.length === 0) return; + + await tx.outboxEvent.createMany({ + data: events.map((event) => ({ + eventType: event.eventType, + topic: event.topic, + key: event.key, + payload: event.payload as Prisma.JsonObject, + aggregateId: event.aggregateId, + aggregateType: event.aggregateType, + status: OutboxStatus.PENDING, + })), + }); + } + + async saveEvents(events: OutboxEventData[]): Promise { + if (events.length === 0) return; + + await this.prisma.outboxEvent.createMany({ + data: events.map((event) => ({ + eventType: event.eventType, + topic: event.topic, + key: event.key, + payload: event.payload as Prisma.JsonObject, + aggregateId: event.aggregateId, + aggregateType: event.aggregateType, + status: OutboxStatus.PENDING, + })), + }); + } + + async findPendingEvents(limit: number = 100): Promise { + const events = await this.prisma.outboxEvent.findMany({ + where: { status: OutboxStatus.PENDING }, + orderBy: { createdAt: 'asc' }, + take: limit, + }); + return events.map((e) => this.mapToOutboxEvent(e)); + } + + async findEventsForRetry(limit: number = 50): Promise { + const now = new Date(); + const events = await this.prisma.outboxEvent.findMany({ + where: { + status: OutboxStatus.FAILED, + retryCount: { lt: 5 }, + nextRetryAt: { lte: now }, + }, + orderBy: { nextRetryAt: 'asc' }, + take: limit, + }); + return events.map((e) => this.mapToOutboxEvent(e)); + } + + async markAsSent(id: bigint): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxStatus.SENT, + publishedAt: new Date(), + }, + }); + } + + async markAsConfirmed(eventId: string, eventType?: string): Promise { + const whereClause: Prisma.OutboxEventWhereInput = { + aggregateId: eventId, + status: OutboxStatus.SENT, + }; + if (eventType) { + whereClause.eventType = eventType; + } + + const result = await this.prisma.outboxEvent.updateMany({ + where: whereClause, + data: { status: OutboxStatus.CONFIRMED }, + }); + + if (result.count > 0) { + this.logger.log(`[OUTBOX] Event ${eventId} (${eventType || 'all types'}) confirmed`); + return true; + } + return false; + } + + async markAsConfirmedById(id: bigint): Promise { + await this.prisma.outboxEvent.update({ + where: { id }, + data: { status: OutboxStatus.CONFIRMED }, + }); + } + + async findSentEventsTimedOut(timeoutMinutes: number = 5, limit: number = 50): Promise { + const cutoffTime = new Date(); + cutoffTime.setMinutes(cutoffTime.getMinutes() - timeoutMinutes); + + const events = await this.prisma.outboxEvent.findMany({ + where: { + status: OutboxStatus.SENT, + publishedAt: { lt: cutoffTime }, + retryCount: { lt: 5 }, + }, + orderBy: { publishedAt: 'asc' }, + take: limit, + }); + return events.map((e) => this.mapToOutboxEvent(e)); + } + + async resetSentToPending(id: bigint): Promise { + const event = await this.prisma.outboxEvent.findUnique({ where: { id } }); + if (!event) return; + + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxStatus.PENDING, + retryCount: event.retryCount + 1, + lastError: 'Consumer confirmation timeout', + }, + }); + } + + async markAsFailed(id: bigint, error: string): Promise { + const event = await this.prisma.outboxEvent.findUnique({ where: { id } }); + if (!event) return; + + const newRetryCount = event.retryCount + 1; + const delayMinutes = Math.pow(2, newRetryCount - 1); + const nextRetryAt = new Date(); + nextRetryAt.setMinutes(nextRetryAt.getMinutes() + delayMinutes); + + await this.prisma.outboxEvent.update({ + where: { id }, + data: { + status: OutboxStatus.FAILED, + retryCount: newRetryCount, + lastError: error, + nextRetryAt: newRetryCount >= event.maxRetries ? null : nextRetryAt, + }, + }); + } + + async cleanupOldEvents(retentionDays: number = 7): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + + const result = await this.prisma.outboxEvent.deleteMany({ + where: { + status: OutboxStatus.CONFIRMED, + publishedAt: { lt: cutoffDate }, + }, + }); + return result.count; + } + + async getStats(): Promise<{ + pending: number; + sent: number; + confirmed: number; + failed: number; + }> { + const [pending, sent, confirmed, failed] = await Promise.all([ + this.prisma.outboxEvent.count({ where: { status: OutboxStatus.PENDING } }), + this.prisma.outboxEvent.count({ where: { status: OutboxStatus.SENT } }), + this.prisma.outboxEvent.count({ where: { status: OutboxStatus.CONFIRMED } }), + this.prisma.outboxEvent.count({ where: { status: OutboxStatus.FAILED } }), + ]); + return { pending, sent, confirmed, failed }; + } + + private mapToOutboxEvent(record: any): OutboxEvent { + return { + id: record.id, + eventType: record.eventType, + topic: record.topic, + key: record.key, + payload: record.payload as Record, + aggregateId: record.aggregateId, + aggregateType: record.aggregateType, + status: record.status as OutboxStatus, + retryCount: record.retryCount, + maxRetries: record.maxRetries, + lastError: record.lastError, + createdAt: record.createdAt, + publishedAt: record.publishedAt, + nextRetryAt: record.nextRetryAt, + }; + } +} diff --git a/backend/services/transfer-service/src/infrastructure/persistence/repositories/transfer-order.repository.impl.ts b/backend/services/transfer-service/src/infrastructure/persistence/repositories/transfer-order.repository.impl.ts new file mode 100644 index 00000000..cc6627a8 --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/persistence/repositories/transfer-order.repository.impl.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { TransferOrderMapper } from '../mappers/transfer-order.mapper'; +import { TransferOrder } from '../../../domain/aggregates/transfer-order.aggregate'; +import { ITransferOrderRepository } from '../../../domain/repositories/transfer-order.repository.interface'; + +@Injectable() +export class TransferOrderRepositoryImpl implements ITransferOrderRepository { + constructor(private readonly prisma: PrismaService) {} + + async findById(id: bigint): Promise { + const record = await this.prisma.transferOrder.findUnique({ + where: { id }, + }); + return record ? TransferOrderMapper.toDomain(record) : null; + } + + async findByTransferOrderNo(transferOrderNo: string): Promise { + const record = await this.prisma.transferOrder.findUnique({ + where: { transferOrderNo }, + }); + return record ? TransferOrderMapper.toDomain(record) : null; + } + + async findBySellerUserId( + userId: bigint, + options?: { limit?: number; offset?: number }, + ): Promise { + const records = await this.prisma.transferOrder.findMany({ + where: { sellerUserId: userId }, + orderBy: { createdAt: 'desc' }, + take: options?.limit ?? 20, + skip: options?.offset ?? 0, + }); + return records.map(TransferOrderMapper.toDomain); + } + + async findByBuyerUserId( + userId: bigint, + options?: { limit?: number; offset?: number }, + ): Promise { + const records = await this.prisma.transferOrder.findMany({ + where: { buyerUserId: userId }, + orderBy: { createdAt: 'desc' }, + take: options?.limit ?? 20, + skip: options?.offset ?? 0, + }); + return records.map(TransferOrderMapper.toDomain); + } + + async findBySourceOrderNo(sourceOrderNo: string): Promise { + const records = await this.prisma.transferOrder.findMany({ + where: { sourceOrderNo }, + orderBy: { createdAt: 'desc' }, + }); + return records.map(TransferOrderMapper.toDomain); + } + + async findByStatus(status: string, limit: number = 100): Promise { + const records = await this.prisma.transferOrder.findMany({ + where: { status }, + orderBy: { createdAt: 'asc' }, + take: limit, + }); + return records.map(TransferOrderMapper.toDomain); + } + + async countByUserAsSellerOrBuyer(userId: bigint): Promise { + return this.prisma.transferOrder.count({ + where: { + OR: [ + { sellerUserId: userId }, + { buyerUserId: userId }, + ], + }, + }); + } +} diff --git a/backend/services/transfer-service/src/infrastructure/persistence/unit-of-work.ts b/backend/services/transfer-service/src/infrastructure/persistence/unit-of-work.ts new file mode 100644 index 00000000..a6a1623a --- /dev/null +++ b/backend/services/transfer-service/src/infrastructure/persistence/unit-of-work.ts @@ -0,0 +1,162 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService, TransactionClient } from './prisma/prisma.service'; +import { TransferOrder } from '../../domain/aggregates/transfer-order.aggregate'; +import { TransferOrderMapper } from './mappers/transfer-order.mapper'; +import { OutboxEventData } from './repositories/outbox.repository'; + +/** + * 工作单元 - 用于管理跨多个聚合根的数据库事务 + */ +@Injectable() +export class UnitOfWork { + constructor(private readonly prisma: PrismaService) {} + + /** + * 在数据库事务中执行操作 + */ + async executeInTransaction( + fn: (uow: TransactionalUnitOfWork) => Promise, + options?: { + maxWait?: number; + timeout?: number; + isolationLevel?: Prisma.TransactionIsolationLevel; + }, + ): Promise { + return this.prisma.executeTransaction(async (tx) => { + const transactionalUow = new TransactionalUnitOfWork(tx); + return fn(transactionalUow); + }, options); + } +} + +/** + * 事务性工作单元 - 在事务上下文中提供聚合根的持久化操作 + * + * 支持 Outbox Pattern:在同一个事务中保存业务数据和事件数据 + */ +export class TransactionalUnitOfWork { + private readonly logger = new Logger(TransactionalUnitOfWork.name); + private pendingOutboxEvents: OutboxEventData[] = []; + + constructor(private readonly tx: TransactionClient) {} + + /** + * 添加 Outbox 事件 + */ + addOutboxEvent(event: OutboxEventData): void { + this.pendingOutboxEvents.push(event); + } + + /** + * 批量添加 Outbox 事件 + */ + addOutboxEvents(events: OutboxEventData[]): void { + this.pendingOutboxEvents.push(...events); + } + + /** + * 提交所有待发送的 Outbox 事件 + */ + async commitOutboxEvents(): Promise { + if (this.pendingOutboxEvents.length === 0) return; + + await this.tx.outboxEvent.createMany({ + data: this.pendingOutboxEvents.map((event) => ({ + eventType: event.eventType, + topic: event.topic, + key: event.key, + payload: event.payload as Prisma.JsonObject, + aggregateId: event.aggregateId, + aggregateType: event.aggregateType, + status: 'PENDING', + })), + }); + + this.logger.log(`[OUTBOX] Committed ${this.pendingOutboxEvents.length} outbox events`); + this.pendingOutboxEvents = []; + } + + /** + * 保存转让订单 + */ + async saveTransferOrder(order: TransferOrder): Promise { + const data = TransferOrderMapper.toPersistence(order); + + if (order.id) { + await this.tx.transferOrder.update({ + where: { id: order.id }, + data: { + status: data.status, + sagaStep: data.sagaStep, + failReason: data.failReason, + retryCount: data.retryCount, + sellerConfirmedAt: data.sellerConfirmedAt, + paymentFrozenAt: data.paymentFrozenAt, + treesLockedAt: data.treesLockedAt, + ownershipTransferredAt: data.ownershipTransferredAt, + contributionAdjustedAt: data.contributionAdjustedAt, + statsUpdatedAt: data.statsUpdatedAt, + paymentSettledAt: data.paymentSettledAt, + completedAt: data.completedAt, + cancelledAt: data.cancelledAt, + rolledBackAt: data.rolledBackAt, + }, + }); + } else { + const created = await this.tx.transferOrder.create({ + data: { + transferOrderNo: data.transferOrderNo, + sellerUserId: data.sellerUserId, + sellerAccountSequence: data.sellerAccountSequence, + buyerUserId: data.buyerUserId, + buyerAccountSequence: data.buyerAccountSequence, + sourceOrderNo: data.sourceOrderNo, + sourceAdoptionId: data.sourceAdoptionId, + treeCount: data.treeCount, + contributionPerTree: data.contributionPerTree, + originalAdoptionDate: data.originalAdoptionDate, + originalExpireDate: data.originalExpireDate, + selectedProvince: data.selectedProvince, + selectedCity: data.selectedCity, + transferPrice: data.transferPrice, + platformFeeRate: data.platformFeeRate, + platformFeeAmount: data.platformFeeAmount, + sellerReceiveAmount: data.sellerReceiveAmount, + status: data.status, + sagaStep: data.sagaStep, + }, + }); + order.setId(created.id); + } + } + + /** + * 记录状态变更日志 + */ + async logStatusChange(params: { + transferOrderNo: string; + fromStatus: string; + toStatus: string; + fromSagaStep: string; + toSagaStep: string; + operatorType: string; + operatorId?: string; + remark?: string; + }): Promise { + await this.tx.transferStatusLog.create({ + data: { + transferOrderNo: params.transferOrderNo, + fromStatus: params.fromStatus, + toStatus: params.toStatus, + fromSagaStep: params.fromSagaStep, + toSagaStep: params.toSagaStep, + operatorType: params.operatorType, + operatorId: params.operatorId, + remark: params.remark, + }, + }); + } +} + +export const UNIT_OF_WORK = Symbol('UnitOfWork'); diff --git a/backend/services/transfer-service/src/main.ts b/backend/services/transfer-service/src/main.ts new file mode 100644 index 00000000..d08d8a07 --- /dev/null +++ b/backend/services/transfer-service/src/main.ts @@ -0,0 +1,88 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const logger = new Logger('Bootstrap'); + const app = await NestFactory.create(AppModule); + + // 全局前缀 + app.setGlobalPrefix('api/v1'); + + // 全局验证管道 + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { enableImplicitConversion: true }, + }), + ); + + // CORS 配置 + app.enableCors({ + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + credentials: true, + }); + + // Swagger API 文档 + const config = new DocumentBuilder() + .setTitle('Transfer Service API') + .setDescription('RWA 榴莲皇后平台树转让服务 API') + .setVersion('1.0.0') + .addBearerAuth() + .addTag('转让订单', '转让订单相关接口') + .addTag('管理端', '管理端转让相关接口') + .addTag('健康检查', '服务健康检查接口') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + + // Kafka 微服务配置 + const kafkaBrokers = process.env.KAFKA_BROKERS?.split(',') || ['localhost:9092']; + const kafkaGroupId = process.env.KAFKA_GROUP_ID || 'transfer-service-group'; + + // 微服务 1: 用于接收 ACK 确认消息 + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: 'transfer-service-ack', + brokers: kafkaBrokers, + }, + consumer: { + groupId: `${kafkaGroupId}-ack`, + }, + }, + }); + + // 微服务 2: 用于接收各服务的 Saga 步骤确认事件 + app.connectMicroservice({ + transport: Transport.KAFKA, + options: { + client: { + clientId: 'transfer-service-saga-events', + brokers: kafkaBrokers, + }, + consumer: { + groupId: `${kafkaGroupId}-saga-events`, + }, + }, + }); + + // 启动所有 Kafka 微服务 + await app.startAllMicroservices(); + logger.log('Kafka microservices started (ACK + Saga Events)'); + + const port = process.env.APP_PORT || 3013; + await app.listen(port); + + logger.log(`Transfer Service is running on port ${port}`); + logger.log(`Swagger docs: http://localhost:${port}/api/docs`); +} + +bootstrap(); diff --git a/backend/services/transfer-service/src/shared/filters/global-exception.filter.ts b/backend/services/transfer-service/src/shared/filters/global-exception.filter.ts new file mode 100644 index 00000000..5a84ca95 --- /dev/null +++ b/backend/services/transfer-service/src/shared/filters/global-exception.filter.ts @@ -0,0 +1,40 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = '服务器内部错误'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + message = + typeof exceptionResponse === 'string' + ? exceptionResponse + : (exceptionResponse as any).message || exception.message; + } else if (exception instanceof Error) { + message = exception.message; + this.logger.error(`Unhandled error: ${exception.message}`, exception.stack); + } + + response.status(status).json({ + statusCode: status, + message: Array.isArray(message) ? message : [message], + timestamp: new Date().toISOString(), + }); + } +} diff --git a/backend/services/transfer-service/src/shared/filters/index.ts b/backend/services/transfer-service/src/shared/filters/index.ts new file mode 100644 index 00000000..c3ec44dc --- /dev/null +++ b/backend/services/transfer-service/src/shared/filters/index.ts @@ -0,0 +1 @@ +export * from './global-exception.filter'; diff --git a/backend/services/transfer-service/src/shared/index.ts b/backend/services/transfer-service/src/shared/index.ts new file mode 100644 index 00000000..56c36b7f --- /dev/null +++ b/backend/services/transfer-service/src/shared/index.ts @@ -0,0 +1 @@ +export * from './filters'; diff --git a/backend/services/transfer-service/tsconfig.build.json b/backend/services/transfer-service/tsconfig.build.json new file mode 100644 index 00000000..e97f0533 --- /dev/null +++ b/backend/services/transfer-service/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "prisma", "**/*spec.ts"] +} diff --git a/backend/services/transfer-service/tsconfig.json b/backend/services/transfer-service/tsconfig.json new file mode 100644 index 00000000..bd3c3946 --- /dev/null +++ b/backend/services/transfer-service/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/backend/services/wallet-service/src/api/api.module.ts b/backend/services/wallet-service/src/api/api.module.ts index 5a1103cb..743de6aa 100644 --- a/backend/services/wallet-service/src/api/api.module.ts +++ b/backend/services/wallet-service/src/api/api.module.ts @@ -11,6 +11,8 @@ import { } from './controllers'; import { InternalWalletController } from './controllers/internal-wallet.controller'; import { FiatWithdrawalController } from './controllers/fiat-withdrawal.controller'; +// [2026-02-19] 纯新增:转让钱包内部 Controller +import { InternalTransferWalletController } from './controllers/internal-transfer-wallet.controller'; import { WalletApplicationService, FiatWithdrawalApplicationService, SystemWithdrawalApplicationService } from '@/application/services'; import { DepositConfirmedHandler, PlantingCreatedHandler, UserAccountCreatedHandler } from '@/application/event-handlers'; import { WithdrawalStatusHandler } from '@/application/event-handlers/withdrawal-status.handler'; @@ -37,6 +39,7 @@ import { JwtStrategy } from '@/shared/strategies/jwt.strategy'; InternalWalletController, FiatWithdrawalController, SystemWithdrawalController, + InternalTransferWalletController, // [2026-02-19] 纯新增:转让钱包内部端点 ], providers: [ WalletApplicationService, diff --git a/backend/services/wallet-service/src/api/controllers/internal-transfer-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-transfer-wallet.controller.ts new file mode 100644 index 00000000..496885ce --- /dev/null +++ b/backend/services/wallet-service/src/api/controllers/internal-transfer-wallet.controller.ts @@ -0,0 +1,117 @@ +/** + * 转让钱包内部 API(纯新增) + * 供 transfer-service Saga 编排器通过 HTTP 调用 + * + * 路由前缀:/api/v1/internal/wallet/* + * 与 InternalWalletController(/api/v1/wallets/*) 分开,避免路由冲突 + * + * 回滚方式:删除此文件并从 api.module.ts 中移除引用 + */ + +import { Controller, Post, Get, Body, Param, Logger } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { WalletApplicationService } from '@/application/services'; +import { + FreezeForTransferCommand, + UnfreezeForTransferCommand, + SettleTransferPaymentCommand, +} from '@/application/commands'; +import { Public } from '@/shared/decorators'; +import { + FreezeForTransferDto, + UnfreezeForTransferDto, + SettleTransferDto, +} from '../dto/request/transfer-wallet.dto'; + +@ApiTags('Internal Transfer Wallet API') +@Controller('internal/wallet') +export class InternalTransferWalletController { + private readonly logger = new Logger(InternalTransferWalletController.name); + + constructor( + private readonly walletService: WalletApplicationService, + ) {} + + /** + * 冻结买方资金 + * POST /api/v1/internal/wallet/freeze + */ + @Post('freeze') + @Public() + async freezeForTransfer(@Body() dto: FreezeForTransferDto): Promise<{ freezeId: string }> { + this.logger.log(`[freeze] Request: accountSequence=${dto.accountSequence}, amount=${dto.amount}, transferOrderNo=${dto.transferOrderNo}`); + + const command = new FreezeForTransferCommand( + dto.accountSequence, + dto.amount, + dto.transferOrderNo, + dto.reason, + ); + + const result = await this.walletService.freezeForTransfer(command); + this.logger.log(`[freeze] Success: freezeId=${result.freezeId}`); + return result; + } + + /** + * 解冻买方资金(补偿/回滚) + * POST /api/v1/internal/wallet/unfreeze + */ + @Post('unfreeze') + @Public() + async unfreezeForTransfer(@Body() dto: UnfreezeForTransferDto): Promise<{ success: boolean }> { + this.logger.log(`[unfreeze] Request: accountSequence=${dto.accountSequence}, freezeId=${dto.freezeId}, transferOrderNo=${dto.transferOrderNo}`); + + const command = new UnfreezeForTransferCommand( + dto.accountSequence, + dto.freezeId, + dto.transferOrderNo, + dto.reason, + ); + + await this.walletService.unfreezeForTransfer(command); + this.logger.log(`[unfreeze] Success: transferOrderNo=${dto.transferOrderNo}`); + return { success: true }; + } + + /** + * 结算转让资金:买方冻结扣减 → 卖方入账 → 手续费归集 + * POST /api/v1/internal/wallet/settle-transfer + */ + @Post('settle-transfer') + @Public() + async settleTransfer(@Body() dto: SettleTransferDto): Promise<{ success: boolean }> { + this.logger.log(`[settle-transfer] Request: buyer=${dto.buyerAccountSequence}, seller=${dto.sellerAccountSequence}, transferOrderNo=${dto.transferOrderNo}`); + + const command = new SettleTransferPaymentCommand( + dto.buyerAccountSequence, + dto.sellerAccountSequence, + dto.freezeId, + dto.sellerReceiveAmount, + dto.platformFeeAmount, + dto.transferOrderNo, + ); + + await this.walletService.settleTransferPayment(command); + this.logger.log(`[settle-transfer] Success: transferOrderNo=${dto.transferOrderNo}`); + return { success: true }; + } + + /** + * 查询钱包余额 + * GET /api/v1/internal/wallet/balance/:accountSequence + */ + @Get('balance/:accountSequence') + @Public() + async getBalance(@Param('accountSequence') accountSequence: string): Promise<{ + available: string; + frozen: string; + }> { + const query = { userId: accountSequence } as any; + const wallet = await this.walletService.getMyWallet(query); + return { + available: wallet.balances.usdt.available.toString(), + frozen: wallet.balances.usdt.frozen.toString(), + }; + } +} diff --git a/backend/services/wallet-service/src/api/dto/request/transfer-wallet.dto.ts b/backend/services/wallet-service/src/api/dto/request/transfer-wallet.dto.ts new file mode 100644 index 00000000..f2069b84 --- /dev/null +++ b/backend/services/wallet-service/src/api/dto/request/transfer-wallet.dto.ts @@ -0,0 +1,38 @@ +/** + * 树转让钱包操作 DTO(纯新增) + * 供 transfer-service 通过 HTTP 调用 + * + * 回滚方式:删除此文件并从 controller 中移除引用 + */ + +/** + * 冻结买方资金 + */ +export class FreezeForTransferDto { + accountSequence: string; + amount: string; // 字符串格式保持 Decimal 精度 + transferOrderNo: string; + reason: string; +} + +/** + * 解冻买方资金(补偿/回滚) + */ +export class UnfreezeForTransferDto { + accountSequence: string; + freezeId: string; // = transferOrderNo + transferOrderNo: string; + reason: string; +} + +/** + * 结算转让资金:买方冻结 → 卖方实收 + 平台手续费 + */ +export class SettleTransferDto { + buyerAccountSequence: string; + sellerAccountSequence: string; + freezeId: string; + sellerReceiveAmount: string; + platformFeeAmount: string; + transferOrderNo: string; +} diff --git a/backend/services/wallet-service/src/application/commands/freeze-for-transfer.command.ts b/backend/services/wallet-service/src/application/commands/freeze-for-transfer.command.ts new file mode 100644 index 00000000..a4bd82a9 --- /dev/null +++ b/backend/services/wallet-service/src/application/commands/freeze-for-transfer.command.ts @@ -0,0 +1,12 @@ +/** + * 转让冻结资金命令(纯新增) + * 用于在树转让流程中冻结买方资金 + */ +export class FreezeForTransferCommand { + constructor( + public readonly accountSequence: string, + public readonly amount: string, + public readonly transferOrderNo: string, + public readonly reason: string, + ) {} +} diff --git a/backend/services/wallet-service/src/application/commands/index.ts b/backend/services/wallet-service/src/application/commands/index.ts index dc7492ce..e9d244bf 100644 --- a/backend/services/wallet-service/src/application/commands/index.ts +++ b/backend/services/wallet-service/src/application/commands/index.ts @@ -9,3 +9,7 @@ export * from './settle-rewards.command'; export * from './settle-to-balance.command'; export * from './allocate-funds.command'; export * from './request-withdrawal.command'; +// [2026-02-19] 纯新增:树转让钱包命令 +export * from './freeze-for-transfer.command'; +export * from './unfreeze-for-transfer.command'; +export * from './settle-transfer-payment.command'; diff --git a/backend/services/wallet-service/src/application/commands/settle-transfer-payment.command.ts b/backend/services/wallet-service/src/application/commands/settle-transfer-payment.command.ts new file mode 100644 index 00000000..16c21477 --- /dev/null +++ b/backend/services/wallet-service/src/application/commands/settle-transfer-payment.command.ts @@ -0,0 +1,14 @@ +/** + * 转让结算命令(纯新增) + * 单事务内完成:买方冻结扣减 → 卖方入账 → 手续费归集 + */ +export class SettleTransferPaymentCommand { + constructor( + public readonly buyerAccountSequence: string, + public readonly sellerAccountSequence: string, + public readonly freezeId: string, + public readonly sellerReceiveAmount: string, + public readonly platformFeeAmount: string, + public readonly transferOrderNo: string, + ) {} +} diff --git a/backend/services/wallet-service/src/application/commands/unfreeze-for-transfer.command.ts b/backend/services/wallet-service/src/application/commands/unfreeze-for-transfer.command.ts new file mode 100644 index 00000000..0cf63e1a --- /dev/null +++ b/backend/services/wallet-service/src/application/commands/unfreeze-for-transfer.command.ts @@ -0,0 +1,12 @@ +/** + * 转让解冻资金命令(纯新增) + * 用于在树转让失败时解冻买方资金 + */ +export class UnfreezeForTransferCommand { + constructor( + public readonly accountSequence: string, + public readonly freezeId: string, + public readonly transferOrderNo: string, + public readonly reason: string, + ) {} +} diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index d5c0c346..73e89c4a 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -18,6 +18,7 @@ import { ClaimRewardsCommand, SettleRewardsCommand, AllocateFundsCommand, FundAllocationItem, RequestWithdrawalCommand, UpdateWithdrawalStatusCommand, FreezeForPlantingCommand, ConfirmPlantingDeductionCommand, UnfreezeForPlantingCommand, + FreezeForTransferCommand, UnfreezeForTransferCommand, SettleTransferPaymentCommand, } from '@/application/commands'; import { GetMyWalletQuery, GetMyLedgerQuery } from '@/application/queries'; import { DuplicateTransactionError, WalletNotFoundError, OptimisticLockError } from '@/shared/exceptions/domain.exception'; @@ -3945,4 +3946,440 @@ export class WalletApplicationService { return results; } + + // ============================================================ + // [2026-02-19] 纯新增:树转让钱包操作(3 个方法) + // 供 transfer-service Saga 编排器通过 HTTP 调用 + // 回滚方式:删除以下 3 对方法 + 移除 import + // ============================================================ + + /** + * 转让冻结:冻结买方 USDT(从 available 转到 frozen) + * 幂等:同一 transferOrderNo 重复调用返回成功 + */ + async freezeForTransfer(command: FreezeForTransferCommand): Promise<{ freezeId: string }> { + const MAX_RETRIES = 3; + let retries = 0; + + while (retries < MAX_RETRIES) { + try { + return await this.executeFreezeForTransfer(command); + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[freezeForTransfer] Optimistic lock conflict for ${command.transferOrderNo}, retry ${retries}/${MAX_RETRIES}`); + if (retries >= MAX_RETRIES) throw error; + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + throw new Error('Unexpected: exited retry loop without result'); + } + + private async executeFreezeForTransfer(command: FreezeForTransferCommand): Promise<{ freezeId: string }> { + const Decimal = (await import('decimal.js')).default; + const amountDecimal = new Decimal(command.amount); + let walletUserId: bigint | null = null; + + this.logger.log(`[freezeForTransfer] 开始处理: accountSequence=${command.accountSequence}, amount=${command.amount}, transferOrderNo=${command.transferOrderNo}`); + + await this.prisma.$transaction(async (tx) => { + // 幂等性检查 + const existingEntry = await tx.ledgerEntry.findFirst({ + where: { + refOrderId: command.transferOrderNo, + entryType: LedgerEntryType.FREEZE, + }, + }); + + if (existingEntry) { + this.logger.warn(`[freezeForTransfer] Transfer ${command.transferOrderNo} already frozen (idempotent)`); + return; + } + + // 查找钱包 + const walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: command.accountSequence }, + }); + + if (!walletRecord) { + throw new WalletNotFoundError(`accountSequence: ${command.accountSequence}`); + } + + walletUserId = walletRecord.userId; + const currentAvailable = new Decimal(walletRecord.usdtAvailable.toString()); + const currentFrozen = new Decimal(walletRecord.usdtFrozen.toString()); + const currentVersion = walletRecord.version; + + // 余额检查 + if (currentAvailable.lessThan(amountDecimal)) { + throw new BadRequestException( + `余额不足: 需要 ${command.amount} USDT, 可用 ${currentAvailable.toString()} USDT`, + ); + } + + const newAvailable = currentAvailable.minus(amountDecimal); + const newFrozen = currentFrozen.plus(amountDecimal); + + // 乐观锁更新 + const updateResult = await tx.walletAccount.updateMany({ + where: { id: walletRecord.id, version: currentVersion }, + data: { + usdtAvailable: newAvailable, + usdtFrozen: newFrozen, + version: currentVersion + 1, + updatedAt: new Date(), + }, + }); + + if (updateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`); + } + + // 冻结流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: walletRecord.accountSequence, + userId: walletRecord.userId, + entryType: LedgerEntryType.FREEZE, + amount: amountDecimal.negated(), + assetType: 'USDT', + balanceAfter: newAvailable, + refOrderId: command.transferOrderNo, + memo: `Transfer freeze: ${command.reason}`, + }, + }); + + this.logger.log(`[freezeForTransfer] 成功冻结 ${command.amount} USDT for ${command.transferOrderNo}`); + }); + + if (walletUserId) { + await this.walletCacheService.invalidateWallet(walletUserId); + } + + return { freezeId: command.transferOrderNo }; + } + + /** + * 转让解冻:将冻结金额恢复到可用(补偿/回滚) + * 幂等:同一 transferOrderNo 重复调用返回成功 + */ + async unfreezeForTransfer(command: UnfreezeForTransferCommand): Promise { + const MAX_RETRIES = 3; + let retries = 0; + + while (retries < MAX_RETRIES) { + try { + await this.executeUnfreezeForTransfer(command); + return; + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[unfreezeForTransfer] Optimistic lock conflict for ${command.transferOrderNo}, retry ${retries}/${MAX_RETRIES}`); + if (retries >= MAX_RETRIES) throw error; + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + throw new Error('Unexpected: exited retry loop without result'); + } + + private async executeUnfreezeForTransfer(command: UnfreezeForTransferCommand): Promise { + const Decimal = (await import('decimal.js')).default; + let walletUserId: bigint | null = null; + + this.logger.log(`[unfreezeForTransfer] 开始处理: accountSequence=${command.accountSequence}, transferOrderNo=${command.transferOrderNo}`); + + await this.prisma.$transaction(async (tx) => { + // 幂等性检查 + const existingUnfreeze = await tx.ledgerEntry.findFirst({ + where: { + refOrderId: command.transferOrderNo, + entryType: LedgerEntryType.UNFREEZE, + }, + }); + + if (existingUnfreeze) { + this.logger.warn(`[unfreezeForTransfer] Transfer ${command.transferOrderNo} already unfrozen (idempotent)`); + return; + } + + // 安全检查:已结算则不可解冻 + const existingSettle = await tx.ledgerEntry.findFirst({ + where: { + refOrderId: command.transferOrderNo, + entryType: LedgerEntryType.TRANSFER_OUT, + }, + }); + + if (existingSettle) { + throw new BadRequestException(`转让 ${command.transferOrderNo} 已结算,无法解冻`); + } + + // 查找原始冻结记录 + const freezeEntry = await tx.ledgerEntry.findFirst({ + where: { + refOrderId: command.transferOrderNo, + entryType: LedgerEntryType.FREEZE, + }, + }); + + if (!freezeEntry) { + this.logger.warn(`[unfreezeForTransfer] Transfer ${command.transferOrderNo} no freeze record, returning success`); + return; + } + + const frozenAmount = new Decimal(freezeEntry.amount.toString()).abs(); + + // 查找钱包 + const walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: command.accountSequence }, + }); + + if (!walletRecord) { + throw new WalletNotFoundError(`accountSequence: ${command.accountSequence}`); + } + + walletUserId = walletRecord.userId; + const currentAvailable = new Decimal(walletRecord.usdtAvailable.toString()); + const currentFrozen = new Decimal(walletRecord.usdtFrozen.toString()); + const currentVersion = walletRecord.version; + + const newAvailable = currentAvailable.plus(frozenAmount); + const newFrozen = currentFrozen.minus(frozenAmount); + + // 乐观锁更新 + const updateResult = await tx.walletAccount.updateMany({ + where: { id: walletRecord.id, version: currentVersion }, + data: { + usdtAvailable: newAvailable, + usdtFrozen: newFrozen, + version: currentVersion + 1, + updatedAt: new Date(), + }, + }); + + if (updateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`); + } + + // 解冻流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: walletRecord.accountSequence, + userId: walletRecord.userId, + entryType: LedgerEntryType.UNFREEZE, + amount: frozenAmount, + assetType: 'USDT', + balanceAfter: newAvailable, + refOrderId: command.transferOrderNo, + memo: `Transfer unfreeze: ${command.reason}`, + }, + }); + + this.logger.log(`[unfreezeForTransfer] 成功解冻 ${frozenAmount.toString()} USDT for ${command.transferOrderNo}`); + }); + + if (walletUserId) { + await this.walletCacheService.invalidateWallet(walletUserId); + } + } + + /** + * 转让结算:买方冻结扣减 → 卖方入账 → 手续费归集 + * 单事务操作 3 个钱包(买方 + 卖方 + 手续费账户) + * 幂等:同一 transferOrderNo 重复调用返回成功 + */ + async settleTransferPayment(command: SettleTransferPaymentCommand): Promise { + const MAX_RETRIES = 3; + let retries = 0; + + while (retries < MAX_RETRIES) { + try { + await this.executeSettleTransferPayment(command); + return; + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[settleTransferPayment] Optimistic lock conflict for ${command.transferOrderNo}, retry ${retries}/${MAX_RETRIES}`); + if (retries >= MAX_RETRIES) throw error; + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + throw new Error('Unexpected: exited retry loop without result'); + } + + private async executeSettleTransferPayment(command: SettleTransferPaymentCommand): Promise { + const Decimal = (await import('decimal.js')).default; + const sellerAmount = new Decimal(command.sellerReceiveAmount); + const feeAmount = new Decimal(command.platformFeeAmount); + const totalDeduct = sellerAmount.plus(feeAmount); + const affectedUserIds: bigint[] = []; + + this.logger.log(`[settleTransferPayment] 开始结算: transferOrderNo=${command.transferOrderNo}`); + this.logger.log(`[settleTransferPayment] buyer=${command.buyerAccountSequence}, seller=${command.sellerAccountSequence}`); + this.logger.log(`[settleTransferPayment] sellerReceive=${command.sellerReceiveAmount}, fee=${command.platformFeeAmount}`); + + await this.prisma.$transaction(async (tx) => { + // 幂等性检查 + const existingSettle = await tx.ledgerEntry.findFirst({ + where: { + refOrderId: command.transferOrderNo, + entryType: LedgerEntryType.TRANSFER_OUT, + }, + }); + + if (existingSettle) { + this.logger.warn(`[settleTransferPayment] Transfer ${command.transferOrderNo} already settled (idempotent)`); + return; + } + + // ===== 1. 买方:从冻结余额扣减 ===== + const buyerWallet = await tx.walletAccount.findUnique({ + where: { accountSequence: command.buyerAccountSequence }, + }); + + if (!buyerWallet) { + throw new WalletNotFoundError(`buyer accountSequence: ${command.buyerAccountSequence}`); + } + + const buyerFrozen = new Decimal(buyerWallet.usdtFrozen.toString()); + if (buyerFrozen.lessThan(totalDeduct)) { + throw new BadRequestException( + `买方冻结余额不足: 需要 ${totalDeduct.toString()} USDT, 冻结 ${buyerFrozen.toString()} USDT`, + ); + } + + const newBuyerFrozen = buyerFrozen.minus(totalDeduct); + const buyerUpdateResult = await tx.walletAccount.updateMany({ + where: { id: buyerWallet.id, version: buyerWallet.version }, + data: { + usdtFrozen: newBuyerFrozen, + version: buyerWallet.version + 1, + updatedAt: new Date(), + }, + }); + + if (buyerUpdateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for buyer wallet ${buyerWallet.id}`); + } + + affectedUserIds.push(buyerWallet.userId); + + // 买方扣款流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: buyerWallet.accountSequence, + userId: buyerWallet.userId, + entryType: LedgerEntryType.TRANSFER_OUT, + amount: totalDeduct.negated(), + assetType: 'USDT', + balanceAfter: new Decimal(buyerWallet.usdtAvailable.toString()), + refOrderId: command.transferOrderNo, + memo: `Tree transfer payment to ${command.sellerAccountSequence}`, + }, + }); + + // ===== 2. 卖方:可用余额增加 ===== + const sellerWallet = await tx.walletAccount.findUnique({ + where: { accountSequence: command.sellerAccountSequence }, + }); + + if (!sellerWallet) { + throw new WalletNotFoundError(`seller accountSequence: ${command.sellerAccountSequence}`); + } + + const sellerAvailable = new Decimal(sellerWallet.usdtAvailable.toString()); + const newSellerAvailable = sellerAvailable.plus(sellerAmount); + + const sellerUpdateResult = await tx.walletAccount.updateMany({ + where: { id: sellerWallet.id, version: sellerWallet.version }, + data: { + usdtAvailable: newSellerAvailable, + version: sellerWallet.version + 1, + updatedAt: new Date(), + }, + }); + + if (sellerUpdateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for seller wallet ${sellerWallet.id}`); + } + + affectedUserIds.push(sellerWallet.userId); + + // 卖方入账流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: sellerWallet.accountSequence, + userId: sellerWallet.userId, + entryType: LedgerEntryType.TRANSFER_IN, + amount: sellerAmount, + assetType: 'USDT', + balanceAfter: newSellerAvailable, + refOrderId: command.transferOrderNo, + memo: `Tree transfer received from ${command.buyerAccountSequence}`, + }, + }); + + // ===== 3. 手续费归集账户 ===== + if (feeAmount.greaterThan(0)) { + const feeAccountSequence = 'S0000000001'; // 总部账户 + const feeWallet = await tx.walletAccount.findUnique({ + where: { accountSequence: feeAccountSequence }, + }); + + if (feeWallet) { + const feeAvailable = new Decimal(feeWallet.usdtAvailable.toString()); + const newFeeAvailable = feeAvailable.plus(feeAmount); + + const feeUpdateResult = await tx.walletAccount.updateMany({ + where: { id: feeWallet.id, version: feeWallet.version }, + data: { + usdtAvailable: newFeeAvailable, + version: feeWallet.version + 1, + updatedAt: new Date(), + }, + }); + + if (feeUpdateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for fee wallet ${feeWallet.id}`); + } + + affectedUserIds.push(feeWallet.userId); + + // 手续费流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: feeAccountSequence, + userId: feeWallet.userId, + entryType: LedgerEntryType.FEE_COLLECTION, + amount: feeAmount, + assetType: 'USDT', + balanceAfter: newFeeAvailable, + refOrderId: command.transferOrderNo, + memo: `Transfer platform fee from ${command.buyerAccountSequence}`, + }, + }); + } else { + this.logger.warn(`[settleTransferPayment] Fee account ${feeAccountSequence} not found, skipping fee collection`); + } + } + + this.logger.log(`[settleTransferPayment] 结算成功: buyer frozen -${totalDeduct}, seller +${sellerAmount}, fee +${feeAmount}`); + }); + + // 事务成功后,使所有受影响钱包的缓存失效 + for (const userId of affectedUserIds) { + await this.walletCacheService.invalidateWallet(userId); + } + + this.logger.log(`[settleTransferPayment] 转让 ${command.transferOrderNo} 结算完成`); + } } diff --git a/docs/tree-transfer-implementation-plan.md b/docs/tree-transfer-implementation-plan.md new file mode 100644 index 00000000..b5d43a3c --- /dev/null +++ b/docs/tree-transfer-implementation-plan.md @@ -0,0 +1,1719 @@ +# 树转让功能详细实施方案 + +> 版本:v1.0 | 日期:2026-02-18 | 状态:待审批 + +--- + +## 一、概述与设计原则 + +### 1.1 功能目标 + +实现已认种树(状态为 `MINING_ENABLED`)在用户间的所有权转让,包括: +- 持仓变更(planting-service) +- 算力增量调整(contribution-service) +- 团队统计增减(referral-service) +- 资金结算(wallet-service) + +### 1.2 核心设计原则 + +| # | 原则 | 说明 | +|---|------|------| +| 1 | **纯新增** | 各现有服务只新增事件消费者、方法、数据表,不修改已有业务逻辑 | +| 2 | **事务流水型** | 所有算力变动通过追加新流水记录实现,历史记录零修改、零删除 | +| 3 | **保持原始值** | 转让时沿用树的原始 `contributionPerTree`,不按转让日重算 | +| 4 | **历史不追回** | 已分配给卖方链路的历史算力记录保持不变,只从转让时刻起追加对冲/新增流水 | +| 5 | **可回滚** | Saga 编排每一步都有对应的补偿操作 | +| 6 | **奖励不重分** | reward-service 的历史分配记录(USDT 权益)不受转让影响 | +| 7 | **运营算力不动** | 仅运营账户 12% 为全局固定分配不受影响;省公司 1% 和市公司 2% 需随所有权变更调整 | +| 8 | **快照自动适配** | 挖矿快照每日按最新 effectiveContribution 全量生成,无需特殊处理 | + +### 1.3 转让单位 + +- 转让以 **PlantingOrder(认种订单)** 为单位 +- 支持整单转让(全部树)和部分转让(订单内指定棵数) +- 部分转让时,原订单不修改,通过 TransferRecord 追踪已转出的棵数 + +### 1.4 转让资格 + +- 卖方:树状态为 `MINING_ENABLED` 且未被锁定 +- 买方:已完成实名认证的平台用户 +- 同一棵树可多次转让(每次创建新的转让记录) + +--- + +## 二、transfer-service 新微服务设计 + +### 2.1 服务概要 + +| 项目 | 值 | +|------|-----| +| 服务名 | transfer-service | +| 容器名 | rwa-transfer-service | +| 端口 | 3013 | +| Redis DB | 12 | +| 数据库 | rwa_transfer | +| 技术栈 | NestJS 10 + Prisma 5.7 + PostgreSQL 16 | +| Kafka Consumer Group | transfer-service-group | + +### 2.2 数据模型 + +#### TransferOrder(转让订单 - Saga 聚合根) + +```prisma +model TransferOrder { + id BigInt @id @default(autoincrement()) + transferOrderNo String @unique @map("transfer_order_no") @db.VarChar(50) + + // ========== 卖方信息 ========== + sellerUserId BigInt @map("seller_user_id") + sellerAccountSequence String @map("seller_account_sequence") @db.VarChar(20) + + // ========== 买方信息 ========== + buyerUserId BigInt @map("buyer_user_id") + buyerAccountSequence String @map("buyer_account_sequence") @db.VarChar(20) + + // ========== 转让标的 ========== + sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 原认种订单号 + sourceAdoptionId BigInt @map("source_adoption_id") // 原认种ID + treeCount Int @map("tree_count") // 转让棵数 + contributionPerTree Decimal @map("contribution_per_tree") @db.Decimal(20, 10) // 原始每棵算力值 + originalAdoptionDate DateTime @map("original_adoption_date") @db.Date // 原始认种日期 + originalExpireDate DateTime @map("original_expire_date") @db.Date // 原始算力到期日 + selectedProvince String @map("selected_province") @db.VarChar(10) // 树所在省 + selectedCity String @map("selected_city") @db.VarChar(10) // 树所在市 + + // ========== 价格与费用 ========== + transferPrice Decimal @map("transfer_price") @db.Decimal(20, 8) // 转让总价(USDT) + platformFeeRate Decimal @map("platform_fee_rate") @db.Decimal(5, 4) // 平台手续费率 + platformFeeAmount Decimal @map("platform_fee_amount") @db.Decimal(20, 8) // 平台手续费 + sellerReceiveAmount Decimal @map("seller_receive_amount") @db.Decimal(20, 8) // 卖方实收 + + // ========== Saga 状态 ========== + status String @default("PENDING") @map("status") @db.VarChar(30) + sagaStep String @default("INIT") @map("saga_step") @db.VarChar(30) + failReason String? @map("fail_reason") @db.VarChar(500) + retryCount Int @default(0) @map("retry_count") + + // ========== 各步骤确认时间戳 ========== + sellerConfirmedAt DateTime? @map("seller_confirmed_at") + paymentFrozenAt DateTime? @map("payment_frozen_at") + treesLockedAt DateTime? @map("trees_locked_at") + ownershipTransferredAt DateTime? @map("ownership_transferred_at") + contributionAdjustedAt DateTime? @map("contribution_adjusted_at") + statsUpdatedAt DateTime? @map("stats_updated_at") + paymentSettledAt DateTime? @map("payment_settled_at") + completedAt DateTime? @map("completed_at") + cancelledAt DateTime? @map("cancelled_at") + rolledBackAt DateTime? @map("rolled_back_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([sellerUserId]) + @@index([buyerUserId]) + @@index([sourceOrderNo]) + @@index([status]) + @@index([createdAt]) + @@map("transfer_orders") +} +``` + +#### TransferStatusLog(状态变更日志 - 审计) + +```prisma +model TransferStatusLog { + id BigInt @id @default(autoincrement()) + transferOrderNo String @map("transfer_order_no") @db.VarChar(50) + fromStatus String @map("from_status") @db.VarChar(30) + toStatus String @map("to_status") @db.VarChar(30) + fromSagaStep String @map("from_saga_step") @db.VarChar(30) + toSagaStep String @map("to_saga_step") @db.VarChar(30) + operatorType String @map("operator_type") @db.VarChar(20) // USER / SYSTEM / ADMIN + operatorId String? @map("operator_id") @db.VarChar(50) + remark String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + @@index([transferOrderNo]) + @@map("transfer_status_logs") +} +``` + +#### OutboxEvent(复用标准 Outbox 结构) + +与其他服务完全相同的 Outbox 表结构。 + +### 2.3 Saga 状态机 + +#### TransferOrderStatus 枚举 + +``` +PENDING - 待卖方确认 +SELLER_CONFIRMED - 卖方已确认 +PAYMENT_FROZEN - 买方资金已冻结 +TREES_LOCKED - 卖方树已锁定 +OWNERSHIP_TRANSFERRED - 所有权已变更 +CONTRIBUTION_ADJUSTED - 算力已调整 +STATS_UPDATED - 团队统计已更新 +PAYMENT_SETTLED - 资金已结算 +COMPLETED - 转让完成 + +CANCELLED - 已取消(PENDING/SELLER_CONFIRMED 阶段可取消) +FAILED - 失败 +ROLLING_BACK - 补偿中 +ROLLED_BACK - 已回滚 +``` + +#### SagaStep 枚举(编排步骤) + +``` +INIT - 初始化 +FREEZE_BUYER_PAYMENT - 冻结买方资金 +LOCK_SELLER_TREES - 锁定卖方树 +TRANSFER_OWNERSHIP - 变更所有权 +ADJUST_CONTRIBUTION - 调整算力 +UPDATE_TEAM_STATS - 更新团队统计 +SETTLE_PAYMENT - 结算资金 +FINALIZE - 完成 + +COMPENSATE_UNLOCK_TREES - 补偿:解锁树 +COMPENSATE_UNFREEZE - 补偿:解冻资金 +``` + +#### 状态机流转图 + +``` +用户发起转让 + ↓ +PENDING (INIT) + ↓ 卖方确认 +SELLER_CONFIRMED (FREEZE_BUYER_PAYMENT) + ↓ 冻结买方资金 → wallet-service +PAYMENT_FROZEN (LOCK_SELLER_TREES) + ↓ 锁定卖方树 → planting-service +TREES_LOCKED (TRANSFER_OWNERSHIP) + ↓ 变更所有权 → planting-service(发布事件) +OWNERSHIP_TRANSFERRED (ADJUST_CONTRIBUTION) + ↓ 等待 contribution-service 确认(事件驱动) +CONTRIBUTION_ADJUSTED (UPDATE_TEAM_STATS) + ↓ 等待 referral-service 确认(事件驱动) +STATS_UPDATED (SETTLE_PAYMENT) + ↓ 结算资金 → wallet-service +PAYMENT_SETTLED (FINALIZE) + ↓ +COMPLETED + +失败补偿路径(任何步骤失败): + FAILED → ROLLING_BACK + → COMPENSATE_UNLOCK_TREES(如果树已锁定) + → COMPENSATE_UNFREEZE(如果资金已冻结) + → ROLLED_BACK +``` + +### 2.4 API 设计 + +#### 用户端 API + +``` +POST /transfers - 发起转让(买方向卖方购买) +POST /transfers/:id/seller-confirm - 卖方确认 +POST /transfers/:id/cancel - 取消转让 +GET /transfers - 查询我的转让记录(买/卖) +GET /transfers/:id - 转让详情 +``` + +#### 管理端 API + +``` +GET /admin/transfers - 转让列表(筛选、分页) +GET /admin/transfers/:id - 转让详情 +GET /admin/transfers/stats - 转让统计 +POST /admin/transfers/:id/force-cancel - 强制取消 +``` + +### 2.5 转让号生成规则 + +``` +TRF + YYMMDD + 随机字符串(8位) +示例:TRF260218A3B7C9D1 +``` + +--- + +## 三、planting-service 纯新增设计 + +### 3.1 新增数据表 + +#### PlantingTransferRecord(转让记录) + +```prisma +model PlantingTransferRecord { + id BigInt @id @default(autoincrement()) + transferOrderNo String @unique @map("transfer_order_no") @db.VarChar(50) + + // ========== 原订单信息 ========== + sourceOrderNo String @map("source_order_no") @db.VarChar(50) + sourceOrderId BigInt @map("source_order_id") + + // ========== 卖方 ========== + fromUserId BigInt @map("from_user_id") + fromAccountSequence String @map("from_account_sequence") @db.VarChar(20) + + // ========== 买方 ========== + toUserId BigInt @map("to_user_id") + toAccountSequence String @map("to_account_sequence") @db.VarChar(20) + + // ========== 转让内容 ========== + treeCount Int @map("tree_count") + selectedProvince String @map("selected_province") @db.VarChar(10) + selectedCity String @map("selected_city") @db.VarChar(10) + contributionPerTree Decimal @map("contribution_per_tree") @db.Decimal(20, 10) + originalAdoptionDate DateTime @map("original_adoption_date") @db.Date + originalExpireDate DateTime @map("original_expire_date") @db.Date + + // ========== 状态 ========== + status String @default("LOCKED") @map("status") @db.VarChar(20) + // LOCKED → TRANSFERRED → ROLLED_BACK + + lockedAt DateTime? @map("locked_at") + transferredAt DateTime? @map("transferred_at") + rolledBackAt DateTime? @map("rolled_back_at") + createdAt DateTime @default(now()) @map("created_at") + + @@index([sourceOrderNo]) + @@index([fromUserId]) + @@index([toUserId]) + @@index([status]) + @@map("planting_transfer_records") +} +``` + +### 3.2 PlantingOrder 新增字段 + +在 PlantingOrder 表上新增一个字段用于标记转让锁定状态(通过 Prisma migration 新增列): + +```prisma +// 新增字段(可空,不影响现有数据) +transferLockedCount Int @default(0) @map("transfer_locked_count") // 被锁定的转让棵数 +``` + +**可转让棵数** = `treeCount - transferLockedCount - 已完成转让的棵数` + +已完成转让的棵数通过查询 `PlantingTransferRecord` 表(status = TRANSFERRED)聚合得到。 + +### 3.3 新增事件消费处理器 + +#### transfer-lock.handler.ts(锁定树) + +- **消费 Topic**:`transfer.trees.lock` +- **处理逻辑**: + 1. 验证卖方持有该订单且有足够可转让棵数 + 2. 创建 `PlantingTransferRecord`(status = LOCKED) + 3. 更新 `PlantingOrder.transferLockedCount += treeCount` + 4. 发布确认事件到 `transfer.trees.lock.ack` + +#### transfer-execute.handler.ts(执行所有权变更) + +- **消费 Topic**:`transfer.ownership.execute` +- **处理逻辑**: + 1. 查找对应的 `PlantingTransferRecord` + 2. 更新 `PlantingTransferRecord.status = TRANSFERRED` + 3. 更新卖方 `PlantingPosition`: + - `effectiveTreeCount -= treeCount` + - `totalTreeCount -= treeCount` + 4. 更新卖方 `PositionDistribution`(对应省市的持仓数 -= treeCount) + 5. 更新/创建买方 `PlantingPosition`: + - `effectiveTreeCount += treeCount` + - `totalTreeCount += treeCount` + 6. 更新/创建买方 `PositionDistribution`(对应省市的持仓数 += treeCount) + 7. 发布 Outbox 事件(3 个): + - `planting.transfer.completed`(通知 transfer-service) + - `planting.ownership.removed`(通知 contribution + referral) + - `planting.ownership.added`(通知 contribution + referral) + +#### transfer-rollback.handler.ts(回滚锁定) + +- **消费 Topic**:`transfer.trees.unlock` +- **处理逻辑**: + 1. 查找对应的 `PlantingTransferRecord` + 2. 更新 `PlantingTransferRecord.status = ROLLED_BACK` + 3. 更新 `PlantingOrder.transferLockedCount -= treeCount` + 4. 发布确认事件到 `transfer.trees.unlock.ack` + +### 3.4 新增 Outbox 事件 + +| 事件 | Topic | AggregateType | Payload | +|------|-------|---------------|---------| +| `PlantingTransferLocked` | `planting.transfer.locked` | `PlantingTransfer` | transferOrderNo, sourceOrderNo, treeCount | +| `PlantingTransferCompleted` | `planting.transfer.completed` | `PlantingTransfer` | transferOrderNo, 完整卖方/买方信息 | +| `PlantingOwnershipRemoved` | `planting.ownership.removed` | `PlantingTransfer` | 卖方信息 + 树信息 | +| `PlantingOwnershipAdded` | `planting.ownership.added` | `PlantingTransfer` | 买方信息 + 树信息 | +| `PlantingTransferRolledBack` | `planting.transfer.rolledback` | `PlantingTransfer` | transferOrderNo | + +--- + +## 四、referral-service 纯新增设计 + +### 4.1 新增事件处理器 + +#### planting-transferred.handler.ts + +- **消费 Topic**:`planting.ownership.removed` + `planting.ownership.added` +- **Consumer Group**:`referral-service-transfer` + +### 4.2 处理 planting.ownership.removed(卖方减少) + +```typescript +async handleOwnershipRemoved(event: PlantingOwnershipRemovedEvent): Promise { + // 1. 更新卖方个人认种统计 + const sellerStats = await this.teamStatsRepo.findByUserId(event.sellerUserId); + sellerStats.removePersonalPlanting(event.treeCount, event.provinceCode, event.cityCode); + // selfPlantingCount -= treeCount + // selfPlantingAmount -= treeCount * pricePerTree + // provinceCityDistribution 对应省市 -= treeCount + await this.teamStatsRepo.save(sellerStats); + + // 2. 获取卖方的上级链 + const relationship = await this.referralRepo.findByUserId(event.sellerUserId); + const ancestors = relationship.getAllAncestorIds(); + + // 3. 批量更新上级团队统计(负数 delta) + const updates = ancestors.map((ancestorId, i) => ({ + userId: ancestorId, + countDelta: -event.treeCount, // 关键:负数 + provinceCode: event.provinceCode, + cityCode: event.cityCode, + fromDirectReferralId: i === 0 ? event.sellerUserId : ancestors[0], + })); + + await this.teamStatsRepo.batchUpdateTeamCountsForTransfer(updates); + // totalTeamPlantingCount -= treeCount + // DirectReferral.teamPlantingCount -= treeCount(increment 负数) + // 重新扫描所有直推的 teamPlantingCount 找新 max(关键修正) + // effectivePlantingCountForRanking = totalTeamPlantingCount - newMax +} +``` + +### 4.3 处理 planting.ownership.added(买方增加) + +```typescript +async handleOwnershipAdded(event: PlantingOwnershipAddedEvent): Promise { + // 1. 更新买方个人认种统计 + const buyerStats = await this.teamStatsRepo.findByUserId(event.buyerUserId); + buyerStats.addPersonalPlanting(event.treeCount, event.provinceCode, event.cityCode); + await this.teamStatsRepo.save(buyerStats); + + // 2. 获取买方的上级链 + const relationship = await this.referralRepo.findByUserId(event.buyerUserId); + const ancestors = relationship.getAllAncestorIds(); + + // 3. 批量更新上级团队统计(正数 delta,复用现有 batchUpdateTeamCounts) + const updates = ancestors.map((ancestorId, i) => ({ + userId: ancestorId, + countDelta: event.treeCount, // 正数 + provinceCode: event.provinceCode, + cityCode: event.cityCode, + fromDirectReferralId: i === 0 ? event.buyerUserId : ancestors[0], + })); + + await this.teamStatsRepo.batchUpdateTeamCounts(updates); // 复用现有方法 +} +``` + +### 4.4 新增方法 + +#### TeamStatistics.removePersonalPlanting()(聚合根新增方法) + +```typescript +removePersonalPlanting(count: number, provinceCode: string, cityCode: string): void { + this._selfPlantingCount = Math.max(0, this._selfPlantingCount - count); + this._selfPlantingAmount = this._selfPlantingAmount.minus(count * PRICE_PER_TREE); + + // 更新省市分布 + const dist = this._provinceCityDistribution; + if (dist[provinceCode]?.[cityCode]) { + dist[provinceCode][cityCode] = Math.max(0, dist[provinceCode][cityCode] - count); + } + + this._updatedAt = new Date(); +} +``` + +#### TeamStatisticsRepository.batchUpdateTeamCountsForTransfer()(新增方法) + +与现有 `batchUpdateTeamCounts` 逻辑类似,但在处理负数 delta 时: + +```typescript +// 关键差异:负数 delta 时重新扫描所有直推找 max +if (update.countDelta < 0) { + // 更新直推的 teamPlantingCount(允许负数 increment) + await tx.directReferral.updateMany({ + where: { referrerId: update.userId, referralId: update.fromDirectReferralId }, + data: { teamPlantingCount: { increment: update.countDelta } }, + }); + + // 重新扫描所有直推的 teamPlantingCount 找最大值 + const allDirectReferrals = await tx.directReferral.findMany({ + where: { referrerId: update.userId }, + select: { teamPlantingCount: true }, + }); + const newMax = Math.max(0, ...allDirectReferrals.map(r => r.teamPlantingCount)); + + const newTotal = currentStats.totalTeamPlantingCount + update.countDelta; + const newEffective = Math.max(0, newTotal - newMax); + + await tx.teamStatistics.update({ + where: { userId: update.userId }, + data: { + totalTeamPlantingCount: Math.max(0, newTotal), + maxSingleTeamPlantingCount: newMax, + effectivePlantingCountForRanking: newEffective, + }, + }); +} +``` + +### 4.5 发布确认事件 + +处理完成后发布事件到 `referral.transfer.stats-updated`,供 transfer-service 确认 Saga 步骤。 + +--- + +## 五、contribution-service 纯新增设计(核心) + +### 5.1 算力调整原则 + +``` +原则一:历史记录不修改、不删除 +原则二:所有调整通过追加新流水记录实现(对冲式) +原则三:保持原始 contributionPerTree,沿用原认种参数 +原则四:新记录的 expireDate = 原始认种的 expireDate(剩余有效期) +原则五:解锁状态需要更新(影响未来新认种的分配,但不追回历史) +原则六:调整 88%(个人70% + 团队层级7.5% + 个人加成7.5% + 省1% + 市2%),仅运营12% 不动 +原则七:挖矿快照每日按最新 effectiveContribution 自动生成,无需干预 +``` + +### 5.1.1 算力影响范围明确界定 + +| 算力类别 | 比例 | 分配对象 | 转让时需调整? | 原因 | +|---------|------|---------|:---:|------| +| 个人算力 | 70% | 认种人 | **是** | 所有权转移,算力跟着树走 | +| 团队层级 | 7.5% | 认种人 15 级上线 | **是** | 上线链从卖方链路换成买方链路 | +| 个人加成 | 7.5% | 认种人(T1/T2/T3) | **是** | 所有权转移,加成跟着树走 | +| 省公司 | 1% | 按树所在省的系统账户 | **是** | 所有权变更,省公司授权体系随新所有者调整 | +| 市公司 | 2% | 按树所在市的系统账户 | **是** | 所有权变更,市公司授权体系随新所有者调整 | +| 运营账户 | 12% | OPERATION 系统账户 | **否** | 全局固定分配,与用户和地域无关 | + +**结论**:转让需处理 88% 的算力调整,仅运营账户 12% 完全不受影响。 + +### 5.2 新增 sourceType 枚举值 + +在现有 ContributionRecord 的 sourceType 基础上新增: + +``` +// 转让转出(卖方侧,金额为负) +TRANSFER_OUT_PERSONAL - 卖方个人算力转出 +TRANSFER_OUT_TEAM_LEVEL - 卖方上线团队层级算力转出 +TRANSFER_OUT_BONUS - 卖方个人加成算力转出 + +// 转让转入(买方侧,金额为正) +TRANSFER_IN_PERSONAL - 买方个人算力转入 +TRANSFER_IN_TEAM_LEVEL - 买方上线团队层级算力转入 +TRANSFER_IN_BONUS - 买方个人加成算力转入 + +// 系统账户调整(省公司 1% + 市公司 2%,随所有权变更) +TRANSFER_OUT_SYSTEM_PROVINCE - 卖方省公司系统账户算力转出 +TRANSFER_OUT_SYSTEM_CITY - 卖方市公司系统账户算力转出 +TRANSFER_IN_SYSTEM_PROVINCE - 买方省公司系统账户算力转入 +TRANSFER_IN_SYSTEM_CITY - 买方市公司系统账户算力转入 + +// 注意:仅运营账户 12% 为全局固定分配,不受转让影响 +``` + +### 5.3 ContributionRecord 新增字段 + +```prisma +// 新增可空字段(不影响现有数据) +transferOrderNo String? @map("transfer_order_no") @db.VarChar(50) // 关联转让订单号 +``` + +### 5.4 卖方算力扣减流水(planting.ownership.removed 事件处理) + +#### 5.4.1 个人算力扣减 + +```typescript +// 创建一条负数流水 +ContributionRecord.create({ + accountSequence: seller.accountSequence, + sourceType: 'TRANSFER_OUT_PERSONAL', + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: seller.accountSequence, + treeCount: transferTreeCount, + baseContribution: originalContributionPerTree, + distributionRate: 0.70, + amount: -(originalContributionPerTree * transferTreeCount * 0.70), // 负数 + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, // 转让日期次日 + expireDate: originalExpireDate, // 原始到期日 + status: 'EFFECTIVE', + remark: `转让转出至 ${buyer.accountSequence}`, +}); + +// 更新卖方 ContributionAccount +sellerAccount.personalContribution -= amount; +sellerAccount.effectiveContribution -= amount; +``` + +#### 5.4.2 卖方上线团队层级扣减 + +对卖方的 15 级上线,每级创建一条负数流水: + +```typescript +for (let level = 1; level <= 15; level++) { + const ancestor = sellerAncestorChain[level - 1]; + if (!ancestor) break; + + const levelAmount = originalContributionPerTree * transferTreeCount * 0.005; + + // 检查当时这一级是否已分配(查询原始 ContributionRecord) + const originalRecord = await findOriginalTeamLevelRecord( + ancestor.accountSequence, originalAdoptionId, level + ); + + if (originalRecord && originalRecord.status === 'EFFECTIVE') { + // 原始记录是已生效的 → 创建负数对冲流水 + ContributionRecord.create({ + accountSequence: ancestor.accountSequence, + sourceType: 'TRANSFER_OUT_TEAM_LEVEL', + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: seller.accountSequence, + treeCount: transferTreeCount, + baseContribution: originalContributionPerTree, + distributionRate: 0.005, + levelDepth: level, + amount: -levelAmount, + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, + status: 'EFFECTIVE', + remark: `转让转出:${seller.accountSequence} → ${buyer.accountSequence}`, + }); + + // 更新上线 ContributionAccount + ancestorAccount.effectiveContribution -= levelAmount; + // 对应层级的 pending/unlocked 字段也需调整 + } + // 如果原始记录在 UnallocatedContribution 中(上线未解锁), + // 则不需要扣减(因为从未分配给该上线),但需标记该 Unallocated 记录已失效 +} +``` + +#### 5.4.3 卖方加成扣减 + +对卖方自身已生效的加成档位,创建负数流水: + +```typescript +for (let tier = 1; tier <= 3; tier++) { + // 查询卖方在原始认种中该档位是否已获得 + const originalBonusRecord = await findOriginalBonusRecord( + seller.accountSequence, originalAdoptionId, tier + ); + + if (originalBonusRecord && originalBonusRecord.status === 'EFFECTIVE') { + const bonusAmount = originalContributionPerTree * transferTreeCount * 0.025; + + ContributionRecord.create({ + accountSequence: seller.accountSequence, + sourceType: 'TRANSFER_OUT_BONUS', + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: seller.accountSequence, + treeCount: transferTreeCount, + baseContribution: originalContributionPerTree, + distributionRate: 0.025, + bonusTier: tier, + amount: -bonusAmount, + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, + status: 'EFFECTIVE', + remark: `转让转出加成T${tier}`, + }); + + sellerAccount.effectiveContribution -= bonusAmount; + } +} +``` + +#### 5.4.4 卖方解锁状态更新 + +```typescript +// 重新计算卖方的 selfPlantingCount +const remainingAdoptions = await syncedDataRepository.countAdoptionsByAccount( + seller.accountSequence, excludeTransferred: true +); + +if (remainingAdoptions === 0) { + // 卖方不再持有任何树 + sellerAccount.hasAdopted = false; + sellerAccount.unlockedLevelDepth = 0; + // unlockedBonusTiers 的 T1 失效,但 T2/T3 取决于 directReferralAdoptedCount + sellerAccount.recalculateUnlockStatus(); +} + +// 检查卖方的推荐人的 directReferralAdoptedCount 是否需要更新 +if (remainingAdoptions === 0) { + const sellerReferrer = await findReferrer(seller.accountSequence); + if (sellerReferrer) { + const newCount = await recalculateDirectReferralAdoptedCount(sellerReferrer.accountSequence); + if (newCount < sellerReferrer.directReferralAdoptedCount) { + sellerReferrerAccount.directReferralAdoptedCount = newCount; + sellerReferrerAccount.recalculateUnlockStatus(); + // 注意:不追回历史分配,只更新状态(影响未来分配) + } + } +} +``` + +### 5.5 买方算力新增流水(planting.ownership.added 事件处理) + +#### 5.5.1 买方个人算力 + +```typescript +ContributionRecord.create({ + accountSequence: buyer.accountSequence, + sourceType: 'TRANSFER_IN_PERSONAL', + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: buyer.accountSequence, // 新的所有者 + treeCount: transferTreeCount, + baseContribution: originalContributionPerTree, // 保持原始值 + distributionRate: 0.70, + amount: originalContributionPerTree * transferTreeCount * 0.70, // 正数 + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, + status: 'EFFECTIVE', + remark: `转让转入来自 ${seller.accountSequence}`, +}); + +buyerAccount.personalContribution += amount; +buyerAccount.effectiveContribution += amount; +``` + +#### 5.5.2 买方上线团队层级分配 + +```typescript +for (let level = 1; level <= 15; level++) { + const ancestor = buyerAncestorChain[level - 1]; + if (!ancestor) { + // 无上线 → UnallocatedContribution(归总部) + createUnallocatedContribution({ + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: buyer.accountSequence, + unallocType: 'TRANSFER_IN_LEVEL_NO_ANCESTOR', + levelDepth: level, + amount: levelAmount, + transferOrderNo: transferOrderNo, + }); + continue; + } + + const ancestorAccount = await findContributionAccount(ancestor.accountSequence); + + if (ancestorAccount && ancestorAccount.unlockedLevelDepth >= level) { + // 上线已解锁 → 直接分配 + ContributionRecord.create({ + accountSequence: ancestor.accountSequence, + sourceType: 'TRANSFER_IN_TEAM_LEVEL', + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: buyer.accountSequence, + levelDepth: level, + amount: levelAmount, // 正数 + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, + status: 'EFFECTIVE', + }); + ancestorAccount.effectiveContribution += levelAmount; + } else { + // 上线未解锁 → UnallocatedContribution(待补发) + createUnallocatedContribution({ + sourceAdoptionId: originalAdoptionId, + sourceAccountSequence: buyer.accountSequence, + unallocType: 'LEVEL_OVERFLOW', + wouldBeAccountSequence: ancestor.accountSequence, + levelDepth: level, + amount: levelAmount, + transferOrderNo: transferOrderNo, + }); + } +} +``` + +#### 5.5.3 买方加成分配 + +```typescript +const buyerUnlockedBonusTiers = calculateUnlockedBonusTiers( + buyerAccount.hasAdopted || true, // 买方获得树后 hasAdopted = true + buyerAccount.directReferralAdoptedCount +); + +for (let tier = 1; tier <= 3; tier++) { + if (buyerUnlockedBonusTiers >= tier) { + ContributionRecord.create({ + accountSequence: buyer.accountSequence, + sourceType: 'TRANSFER_IN_BONUS', + bonusTier: tier, + amount: bonusAmount, + transferOrderNo: transferOrderNo, + // ... + }); + buyerAccount.effectiveContribution += bonusAmount; + } else { + createUnallocatedContribution({ + unallocType: `BONUS_TIER_${tier}_PENDING`, + wouldBeAccountSequence: buyer.accountSequence, + amount: bonusAmount, + transferOrderNo: transferOrderNo, + }); + } +} +``` + +#### 5.5.4 买方解锁状态更新 + +```typescript +// 更新买方 hasAdopted +if (!buyerAccount.hasAdopted) { + buyerAccount.markAsAdopted(); // hasAdopted = true, unlockedLevelDepth = 5, unlockedBonusTiers = 1 +} + +// 检查买方的推荐人的 directReferralAdoptedCount 是否需要更新 +const buyerReferrer = await findReferrer(buyer.accountSequence); +if (buyerReferrer) { + const newCount = await recalculateDirectReferralAdoptedCount(buyerReferrer.accountSequence); + if (newCount > buyerReferrer.directReferralAdoptedCount) { + // 可能触发 checkAndClaimBonus()(复用现有逻辑) + await bonusClaimService.checkAndClaimBonus( + buyerReferrer.accountSequence, + buyerReferrer.directReferralAdoptedCount, + newCount + ); + buyerReferrerAccount.directReferralAdoptedCount = newCount; + buyerReferrerAccount.recalculateUnlockStatus(); + } +} +``` + +### 5.6 系统账户处理 + +**运营账户 12% 不受影响**(全局固定分配)。 + +**省公司 1% 和市公司 2% 需随所有权变更调整**: + +```typescript +// 卖方省市系统账户:创建负数对冲流水 +const provinceAmount = originalContributionPerTree * transferTreeCount * 0.01; +const cityAmount = originalContributionPerTree * transferTreeCount * 0.02; + +// 卖方省公司系统账户扣减 +SystemContributionRecord.create({ + accountType: 'PROVINCE', + regionCode: sellerSelectedProvince, // 原始认种时选择的省 + sourceType: 'TRANSFER_OUT_SYSTEM_PROVINCE', + sourceAdoptionId: originalAdoptionId, + amount: -provinceAmount, // 负数 + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, +}); +// 更新 SystemAccount (PROVINCE, sellerSelectedProvince) 的 contributionBalance + +// 卖方市公司系统账户扣减 +SystemContributionRecord.create({ + accountType: 'CITY', + regionCode: sellerSelectedCity, // 原始认种时选择的市 + sourceType: 'TRANSFER_OUT_SYSTEM_CITY', + sourceAdoptionId: originalAdoptionId, + amount: -cityAmount, + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, +}); + +// 买方省公司系统账户增加 +SystemContributionRecord.create({ + accountType: 'PROVINCE', + regionCode: buyerSelectedProvince, // 买方的省(来自 PlantingOwnershipAdded 事件) + sourceType: 'TRANSFER_IN_SYSTEM_PROVINCE', + sourceAdoptionId: originalAdoptionId, + amount: +provinceAmount, + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, +}); + +// 买方市公司系统账户增加 +SystemContributionRecord.create({ + accountType: 'CITY', + regionCode: buyerSelectedCity, + sourceType: 'TRANSFER_IN_SYSTEM_CITY', + sourceAdoptionId: originalAdoptionId, + amount: +cityAmount, + transferOrderNo: transferOrderNo, + effectiveDate: transferDate, + expireDate: originalExpireDate, +}); +``` + +**注意**:如果卖方和买方在同一省市,省市系统账户的对冲流水互相抵消,净变化为零,但流水记录仍然要写(审计需要)。 + +### 5.7 挖矿快照自动适配 + +**挖矿分配同样无需特殊处理**: + +1. 转让完成后,受影响用户的 `ContributionAccount.effectiveContribution` 已实时更新 +2. 每日定时任务 `SnapshotService.createDailySnapshot()` 读取最新的 `effectiveContribution` +3. 为每个用户生成新的 `contributionRatio = effectiveContribution / networkTotal` +4. `mining-service` 消费快照,按新占比分配挖矿收益 + +**无需任何代码处理**——快照天然支持算力变更。 + +### 5.7 发布确认事件 + +处理完成后发布事件到 `contribution.transfer.adjusted`,供 transfer-service 确认 Saga 步骤。 + +### 5.8 新增文件清单 + +| 文件 | 说明 | +|------|------| +| `src/application/event-handlers/ownership-removed.handler.ts` | 消费 planting.ownership.removed | +| `src/application/event-handlers/ownership-added.handler.ts` | 消费 planting.ownership.added | +| `src/application/services/transfer-adjustment.service.ts` | 转让算力调整编排服务 | +| `src/domain/services/transfer-contribution-calculator.service.ts` | 转让算力计算(查询原始记录 + 生成对冲/新增流水) | + +--- + +## 六、wallet-service 复用方案 + +wallet-service **无需新增代码**,完全复用现有能力: + +### 6.1 冻结买方资金 + +transfer-service 调用 wallet-service 的 `freezeForPlanting` 或通用 `freeze` 接口: + +``` +POST /wallet/freeze +{ + accountSequence: buyer.accountSequence, + amount: transferPrice, + assetType: "USDT", + refOrderId: transferOrderNo, + memo: "树转让冻结" +} +``` + +对应 LedgerEntry:`entryType = FREEZE` + +### 6.2 结算(转账给卖方 + 平台手续费) + +transfer-service 调用 wallet-service 的内部转账: + +``` +// 1. 从买方冻结余额中扣款 +POST /wallet/confirm-deduction +{ + accountSequence: buyer.accountSequence, + amount: transferPrice, + refOrderId: transferOrderNo +} + +// 2. 转账给卖方(实收金额 = 转让价 - 手续费) +POST /wallet/internal-transfer +{ + fromAccountSequence: buyer.accountSequence, + toAccountSequence: seller.accountSequence, + amount: sellerReceiveAmount, + refOrderId: transferOrderNo +} + +// 3. 手续费归集(平台收入) +POST /wallet/fee-collection +{ + fromAccountSequence: buyer.accountSequence, + toAccountSequence: "S0000000006", // 手续费归集账户 + amount: platformFeeAmount, + refOrderId: transferOrderNo +} +``` + +### 6.3 回滚(解冻) + +``` +POST /wallet/unfreeze +{ + accountSequence: buyer.accountSequence, + amount: transferPrice, + refOrderId: transferOrderNo, + memo: "树转让取消,解冻" +} +``` + +--- + +## 七、完整事件契约 + +### 7.1 transfer-service 发出的事件 + +| 事件 | Topic | Key | 消费者 | +|------|-------|-----|--------| +| `TransferTreesLockRequest` | `transfer.trees.lock` | transferOrderNo | planting-service | +| `TransferOwnershipExecute` | `transfer.ownership.execute` | transferOrderNo | planting-service | +| `TransferTreesUnlockRequest` | `transfer.trees.unlock` | transferOrderNo | planting-service | +| `TransferPaymentFreezeRequest` | `transfer.payment.freeze` | transferOrderNo | wallet-service | +| `TransferPaymentSettleRequest` | `transfer.payment.settle` | transferOrderNo | wallet-service | +| `TransferPaymentUnfreezeRequest` | `transfer.payment.unfreeze` | transferOrderNo | wallet-service | + +### 7.2 planting-service 发出的事件 + +| 事件 | Topic | Key | 消费者 | +|------|-------|-----|--------| +| `PlantingTransferLocked` | `planting.transfer.locked` | transferOrderNo | transfer-service | +| `PlantingTransferCompleted` | `planting.transfer.completed` | transferOrderNo | transfer-service | +| `PlantingOwnershipRemoved` | `planting.ownership.removed` | sellerAccountSequence | contribution-service, referral-service | +| `PlantingOwnershipAdded` | `planting.ownership.added` | buyerAccountSequence | contribution-service, referral-service | +| `PlantingTransferRolledBack` | `planting.transfer.rolledback` | transferOrderNo | transfer-service | + +### 7.3 contribution-service 发出的事件 + +| 事件 | Topic | Key | 消费者 | +|------|-------|-----|--------| +| `TransferContributionAdjusted` | `contribution.transfer.adjusted` | transferOrderNo | transfer-service | + +### 7.4 referral-service 发出的事件 + +| 事件 | Topic | Key | 消费者 | +|------|-------|-----|--------| +| `TransferStatsUpdated` | `referral.transfer.stats-updated` | transferOrderNo | transfer-service | + +### 7.5 标准 Payload 格式 + +#### PlantingOwnershipRemoved Payload + +```json +{ + "eventType": "PlantingOwnershipRemoved", + "transferOrderNo": "TRF260218A3B7C9D1", + "sourceOrderNo": "PLT2501150001A2B3", + "sourceAdoptionId": "12345", + "sellerUserId": "100001", + "sellerAccountSequence": "D25011500001", + "treeCount": 2, + "contributionPerTree": "22684.1000000000", + "selectedProvince": "440000", + "selectedCity": "440100", + "originalAdoptionDate": "2025-01-15", + "originalExpireDate": "2027-01-16", + "transferDate": "2026-02-18" +} +``` + +#### PlantingOwnershipAdded Payload + +```json +{ + "eventType": "PlantingOwnershipAdded", + "transferOrderNo": "TRF260218A3B7C9D1", + "sourceOrderNo": "PLT2501150001A2B3", + "sourceAdoptionId": "12345", + "buyerUserId": "200002", + "buyerAccountSequence": "D25121400002", + "treeCount": 2, + "contributionPerTree": "22684.1000000000", + "selectedProvince": "440000", + "selectedCity": "440100", + "originalAdoptionDate": "2025-01-15", + "originalExpireDate": "2027-01-16", + "transferDate": "2026-02-18" +} +``` + +--- + +## 八、完整 Saga 流程图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ transfer-service (Saga 编排器) │ +└────────────────────────────────┬────────────────────────────────────┘ + │ + Step 1: 买方发起转让 │ + ───────────────────── │ + 创建 TransferOrder │ + status = PENDING │ + │ + Step 2: 卖方确认 │ + ───────────────────── │ + status = SELLER_CONFIRMED │ + │ + Step 3: 冻结买方资金 │ ──── wallet-service ──── + ───────────────────── │ freeze(buyer, transferPrice) + status = PAYMENT_FROZEN │ ← 确认冻结成功 + │ + Step 4: 锁定卖方树 │ ──── planting-service ──── + ───────────────────── │ → transfer.trees.lock + status = TREES_LOCKED │ ← planting.transfer.locked + │ + Step 5: 执行所有权变更 │ ──── planting-service ──── + ───────────────────── │ → transfer.ownership.execute + status = OWNERSHIP_ │ ← planting.transfer.completed + TRANSFERRED │ + │ planting-service 同时发布: + │ → planting.ownership.removed + │ → planting.ownership.added + │ + Step 6: 等待算力调整确认 │ ──── contribution-service ──── + ───────────────────── │ 消费 planting.ownership.removed + status = CONTRIBUTION_ │ 消费 planting.ownership.added + ADJUSTED │ ← contribution.transfer.adjusted + │ + Step 7: 等待统计更新确认 │ ──── referral-service ──── + ───────────────────── │ 消费 planting.ownership.removed + status = STATS_UPDATED │ 消费 planting.ownership.added + │ ← referral.transfer.stats-updated + │ + Step 8: 结算资金 │ ──── wallet-service ──── + ───────────────────── │ confirmDeduction(buyer) + status = PAYMENT_SETTLED │ internalTransfer(buyer→seller) + │ feeCollection(buyer→S0000000006) + │ + Step 9: 完成 │ + ───────────────────── │ + status = COMPLETED │ + │ +═══════════════════════════════════════════════════════════════════════ + 补偿路径(从失败步骤反向执行): +═══════════════════════════════════════════════════════════════════════ + │ + 如果 Step 5/6/7 失败: │ + → planting: transfer.trees.unlock (解锁树) + → wallet: unfreeze (解冻资金) │ + │ + 如果 Step 4 失败: │ + → wallet: unfreeze (解冻资金) │ + │ + 如果 Step 3 失败: │ + → 直接标记 FAILED │ +``` + +--- + +## 九、补偿与回滚 + +### 9.1 补偿策略 + +| 失败步骤 | 需要补偿的操作 | 补偿方式 | +|---------|--------------|---------| +| 冻结资金失败 | 无 | 直接标记 FAILED | +| 锁定树失败 | 解冻买方资金 | wallet.unfreeze | +| 所有权变更失败 | 解锁树 + 解冻资金 | planting.unlock + wallet.unfreeze | +| 算力调整超时 | 重试 3 次后人工介入 | 算力为最终一致性,不阻塞流程 | +| 统计更新超时 | 重试 3 次后人工介入 | 统计为最终一致性,不阻塞流程 | +| 资金结算失败 | 反向变更所有权 + 解锁树 + 解冻资金 | 完整回滚 | + +### 9.2 最终一致性策略 + +对于 Step 6(算力)和 Step 7(统计),采用**最终一致性**: + +1. transfer-service 设置超时(5 分钟) +2. 超时后自动重试发送事件(最多 3 次) +3. 3 次重试后标记为 `CONTRIBUTION_PENDING` 或 `STATS_PENDING` +4. 定时任务扫描超时未完成的转让,发送告警 +5. 管理员可通过 Admin API 手动触发重试或强制完成 + +### 9.3 幂等性保证 + +所有事件处理器通过 `ProcessedEvent` 表保证幂等性: + +```typescript +const eventId = `transfer-${transferOrderNo}-${step}`; +const exists = await processedEventRepo.findByEventId(eventId); +if (exists) return; // 已处理,跳过 +``` + +--- + +## 十、Admin Web 管理 + +### 10.1 转让管理页面 + +``` +/admin/transfers - 转让列表 +/admin/transfers/:id - 转让详情 +``` + +#### 列表页功能 + +- 筛选条件:状态、日期范围、卖方/买方账号、转让编号 +- 列表字段:转让编号、卖方、买方、棵数、金额、状态、创建时间 +- 操作按钮:查看详情、强制取消(PENDING/SELLER_CONFIRMED 阶段) + +#### 详情页功能 + +- 基本信息:转让双方、树信息、价格 +- Saga 进度:每个步骤的状态和时间戳 +- 算力流水:关联的 ContributionRecord 列表 +- 团队统计变更:受影响的用户列表 +- 操作日志:所有状态变更记录 + +### 10.2 Sidebar 入口 + +在 admin-web 的 Sidebar 中新增入口: + +``` +认种管理 + ├── 认种订单 + ├── 预种计划 + └── 转让管理 ← 新增 +``` + +--- + +## 十一、Mobile App 用户界面 + +### 11.1 卖方入口 + +在持仓页面(PlantingPosition)的每棵树/每个订单旁新增「转让」按钮: + +``` +我的持仓(10棵树) +├── 订单 PLT250115... (5棵, 广东广州) +│ ├── 状态:挖矿中 +│ └── [转让] +├── 订单 PLT250120... (3棵, 广东深圳) +│ ├── 状态:挖矿中 +│ └── [转让] +└── 订单 PLT250201... (2棵, 北京朝阳) + ├── 状态:挖矿中 + └── [转让] +``` + +### 11.2 转让发起页 + +``` +┌──────────────────────────────┐ +│ 发起转让 │ +│ │ +│ 转让订单:PLT250115... │ +│ 棵数:5 棵(可调整) │ +│ 树位置:广东广州 │ +│ 原始认种日期:2025-01-15 │ +│ 算力到期日:2027-01-16 │ +│ │ +│ 转让价格:______ USDT │ +│ 平台手续费:2% │ +│ 您将收到:______ USDT │ +│ │ +│ 买方账号:D____________ │ +│ │ +│ [确认转让] │ +└──────────────────────────────┘ +``` + +### 11.3 转让记录页 + +``` +我的转让 +├── [标签] 转出 | 转入 | 全部 +├── TRF260218... 转出 5棵 → D25121400002 +│ 状态:已完成 | 收到 15000 USDT +│ 2026-02-18 14:30 +├── TRF260210... 转入 3棵 ← D25011500001 +│ 状态:已完成 | 支付 9500 USDT +│ 2026-02-10 09:15 +└── ... +``` + +--- + +## 十二、实施阶段计划 + +### Phase 1:基础框架(transfer-service + planting-service) + +**目标**:完成转让订单的创建、锁定、所有权变更、资金结算的核心流程。 + +**交付内容**: +1. transfer-service 新微服务搭建 + - 项目脚手架(NestJS + Prisma + Kafka) + - TransferOrder 数据模型 + Migration + - Saga 状态机核心逻辑 + - REST API(用户端 + 管理端) + - Outbox 事件发布 + +2. planting-service 纯新增 + - PlantingTransferRecord 数据表 + Migration + - PlantingOrder.transferLockedCount 新增字段 + - 3 个事件处理器(lock / execute / rollback) + - Outbox 事件发布(ownership.removed / ownership.added) + +3. wallet-service 对接 + - 复用 freeze / unfreeze / internal-transfer 接口 + - 新增 LedgerEntryType: `TRANSFER_FREEZE` / `TRANSFER_PAYMENT` + +4. Docker 配置 + - docker-compose.yml 新增 transfer-service + - Kafka topic 创建脚本 + +**验收标准**: +- 完成转让全流程(不含算力和统计) +- 资金正确冻结、结算、回滚 +- 持仓数量正确变更 + +--- + +### Phase 2:算力调整(contribution-service) + +**目标**:完成算力的事务流水型增量调整。 + +**交付内容**: +1. contribution-service 纯新增 + - 新增 sourceType 枚举值(10 个 TRANSFER_OUT/IN 类型,含系统省市) + - ContributionRecord.transferOrderNo 新增字段 + Migration + - ownership-removed.handler.ts(卖方算力扣减) + - ownership-added.handler.ts(买方算力分配) + - transfer-adjustment.service.ts(编排服务) + - transfer-contribution-calculator.service.ts(计算服务) + +2. 解锁状态联动 + - 卖方 hasAdopted / unlockedLevelDepth / unlockedBonusTiers 重算 + - 买方 hasAdopted / unlockedLevelDepth / unlockedBonusTiers 更新 + - 买方推荐人 directReferralAdoptedCount 更新 + bonus 补发 + +3. 确认事件发布 + - `contribution.transfer.adjusted` 事件 + +**验收标准**: +- 卖方 effectiveContribution 正确减少 +- 买方 effectiveContribution 正确增加 +- 省市系统账户正确调整(卖方省市减少,买方省市增加) +- 全网算力总量守恒(卖方减少 = 买方增加 + UnallocatedContribution) +- 历史 ContributionRecord 零修改 +- 次日快照正确反映新算力 + +--- + +### Phase 3:团队统计(referral-service) + +**目标**:完成团队认种统计的增减和龙虎榜重算。 + +**交付内容**: +1. referral-service 纯新增 + - planting-transferred.handler.ts + - TeamStatistics.removePersonalPlanting() 新增方法 + - TeamStatisticsRepository.batchUpdateTeamCountsForTransfer() 新增方法 + - 龙虎榜 max 重算逻辑修正(负数 delta 时全量扫描) + +2. 确认事件发布 + - `referral.transfer.stats-updated` 事件 + +**验收标准**: +- 卖方链路 totalTeamPlantingCount 正确减少 +- 买方链路 totalTeamPlantingCount 正确增加 +- 龙虎榜排名正确更新 +- selfPlantingCount 正确反映当前持仓 + +--- + +### Phase 4:前端(Admin Web + Mobile App) + +**目标**:完成管理端和用户端的完整交互界面。 + +**交付内容**: +1. Admin Web(Next.js 14) + - 转让管理列表页 + - 转让详情页(含 Saga 进度可视化) + - Sidebar 入口 + - 统计看板 + +2. Mobile App(Flutter) + - 持仓页转让入口 + - 转让发起页 + - 转让记录页(转出/转入) + - 转让状态实时更新 + +**验收标准**: +- 完整的转让用户流程 +- 管理端可查看所有转让记录和状态 +- 异常状态有明确提示 + +--- + +### Phase 5:测试与上线 + +**交付内容**: +1. 单元测试 + - 算力计算正确性(卖方扣减 / 买方分配 / 解锁联动) + - Saga 状态机全路径覆盖 + - 补偿逻辑验证 + +2. 集成测试 + - 完整转让流程 E2E + - 补偿回滚 E2E + - 并发转让冲突测试 + - 部分转让 + 多次转让场景 + +3. 压力测试 + - 大量并发转让请求 + - Kafka 事件积压恢复 + +4. 上线 + - 数据库 Migration 执行 + - 服务部署 + - 灰度放量 + +--- + +## 十三、算力流水示例 + +### 场景:A 转让 2 棵树给 B + +**前提条件**: +- A 有 5 棵树(订单 PLT001),原始 contributionPerTree = 22617 +- A 的上级链:R1 → R2 → R3(3 级,都已解锁) +- B 之前无树,B 的上级链:S1 → S2(2 级,S1 已解锁 5 级,S2 未解锁) +- 转让 2 棵 + +#### 卖方侧新增流水(全部为负数) + +| # | accountSequence | sourceType | amount | remark | +|---|----------------|------------|--------|--------| +| 1 | A | TRANSFER_OUT_PERSONAL | -31,663.80 | 个人 22617×2×0.70 | +| 2 | R1 | TRANSFER_OUT_TEAM_LEVEL (L1) | -226.17 | R1 第1级 22617×2×0.005 | +| 3 | R2 | TRANSFER_OUT_TEAM_LEVEL (L2) | -226.17 | R2 第2级 | +| 4 | R3 | TRANSFER_OUT_TEAM_LEVEL (L3) | -226.17 | R3 第3级 | +| 5 | A | TRANSFER_OUT_BONUS (T1) | -1,130.85 | 加成T1 22617×2×0.025 | +| 6 | A | TRANSFER_OUT_BONUS (T2) | -1,130.85 | 加成T2(如已解锁) | +| 7 | SystemAccount(PROVINCE,440000) | TRANSFER_OUT_SYSTEM_PROVINCE | -452.34 | 省公司 22617×2×0.01 | +| 8 | SystemAccount(CITY,440100) | TRANSFER_OUT_SYSTEM_CITY | -904.68 | 市公司 22617×2×0.02 | + +#### 买方侧新增流水(全部为正数) + +假设买方 B 在北京朝阳(110000/110105),与卖方不同省市: + +| # | accountSequence | sourceType | amount | remark | +|---|----------------|------------|--------|--------| +| 9 | B | TRANSFER_IN_PERSONAL | +31,663.80 | 个人 | +| 10 | S1 | TRANSFER_IN_TEAM_LEVEL (L1) | +226.17 | S1 第1级(已解锁) | +| 11 | S1 | TRANSFER_IN_TEAM_LEVEL (L2) | +226.17 | S1 第2级(已解锁) | +| 12 | S1 | TRANSFER_IN_TEAM_LEVEL (L3) | +226.17 | S1 第3级(已解锁) | +| 13 | S1 | TRANSFER_IN_TEAM_LEVEL (L4) | +226.17 | S1 第4级(已解锁) | +| 14 | S1 | TRANSFER_IN_TEAM_LEVEL (L5) | +226.17 | S1 第5级(已解锁) | +| 15 | → UnallocatedContribution | LEVEL_OVERFLOW (L6) | 226.17 | S2 第6级(未解锁) | +| 16 | → UnallocatedContribution | LEVEL_OVERFLOW (L7) | 226.17 | S2 第7级(未解锁) | +| ... | ... | ... | ... | L8-L15 类似 | +| 17 | B | TRANSFER_IN_BONUS (T1) | +1,130.85 | 加成T1(买方获树后解锁) | +| 18 | → UnallocatedContribution | BONUS_TIER_2_PENDING | 1,130.85 | T2(买方未达条件) | +| 19 | → UnallocatedContribution | BONUS_TIER_3_PENDING | 1,130.85 | T3(买方未达条件) | +| 20 | SystemAccount(PROVINCE,110000) | TRANSFER_IN_SYSTEM_PROVINCE | +452.34 | 买方省公司(北京) | +| 21 | SystemAccount(CITY,110105) | TRANSFER_IN_SYSTEM_CITY | +904.68 | 买方市公司(朝阳) | + +#### 解锁状态联动 + +| 用户 | 变更前 | 变更后 | 说明 | +|------|--------|--------|------| +| A | hasAdopted=true | hasAdopted=true | A 还剩 3 棵 | +| B | hasAdopted=false | hasAdopted=true | 获得树后解锁 | +| B 的推荐人 | directAdoptedCount=N | directAdoptedCount=N+1 | 可能触发升档补发 | + +--- + +## 十四、风险评估与应对 + +| 风险 | 影响 | 应对措施 | +|------|------|---------| +| Saga 步骤失败后补偿不完整 | 资金或树处于不一致状态 | 定时任务扫描异常状态 + Admin 手动介入 | +| 并发转让同一订单 | 超卖 | PlantingOrder.transferLockedCount 乐观锁 + 数据库约束 | +| contribution 事件丢失 | 算力不一致 | Outbox + ProcessedEvent 双保障 + 定时对账 | +| referral 统计出现负数 | 龙虎榜异常 | Math.max(0, ...) 兜底 + 告警 | +| 全网算力总量不守恒 | 挖矿分配异常 | 每日对账脚本:∑增加 = ∑减少 + ∑UnallocatedContribution | +| 大量转让导致 Kafka 积压 | 处理延迟 | 独立 Consumer Group + 限流 | +| 卖方转让后 selfPlantingCount=0 影响推荐人解锁 | 推荐人未来新认种的分配比例降低 | 这是正确的业务行为,无需应对 | + +--- + +## 十五、对账与监控 + +### 15.1 每日对账任务 + +```sql +-- 算力守恒检查 +-- 对于每一笔转让,验证:|卖方扣减| = |买方增加| + |UnallocatedContribution| +SELECT + t.transfer_order_no, + SUM(CASE WHEN cr.source_type LIKE 'TRANSFER_OUT_%' THEN ABS(cr.amount) ELSE 0 END) AS total_out, + SUM(CASE WHEN cr.source_type LIKE 'TRANSFER_IN_%' THEN cr.amount ELSE 0 END) AS total_in, + SUM(CASE WHEN uc.transfer_order_no IS NOT NULL THEN uc.amount ELSE 0 END) AS total_unallocated +FROM transfer_orders t +LEFT JOIN contribution_records cr ON cr.transfer_order_no = t.transfer_order_no +LEFT JOIN unallocated_contributions uc ON uc.transfer_order_no = t.transfer_order_no +WHERE t.status = 'COMPLETED' +GROUP BY t.transfer_order_no +HAVING ABS(total_out - total_in - total_unallocated) > 0.01; +-- 结果应为空(无差异) +``` + +### 15.2 监控告警 + +- Saga 步骤超时(> 5 分钟未完成) +- 补偿失败(ROLLING_BACK 状态超过 10 分钟) +- 算力守恒偏差 > 0.01 +- 团队统计出现负数 +- 每日转让量异常波动 + +--- + +## 十六、积分股转让(2.0 系统附加功能) + +### 16.1 业务规则 + +| 规则 | 说明 | +|------|------| +| **正常情况** | 2.0 系统**不支持**用户间积分股(Share)转让 | +| **触发条件** | 树转让完成(TransferOrder.status = COMPLETED)后自动开启 | +| **参与方限制** | 仅限该笔树转让的买方和卖方双向互转 | +| **有效期** | 树转让完成后 24 小时内 | +| **到期处理** | 24 小时后窗口自动关闭,不可再发起 Share 转让 | +| **转让方向** | 双向:卖方→买方、买方→卖方 均可 | +| **转让次数** | 窗口期内不限次数 | +| **转让数量** | 不超过发起方的可用 Share 余额 | + +### 16.2 数据模型(mining-wallet-service 新增) + +#### ShareTransferWindow(积分股转让窗口) + +```prisma +model ShareTransferWindow { + id String @id @default(uuid()) + transferOrderNo String @unique @map("transfer_order_no") @db.VarChar(50) // 关联的树转让订单号 + + partyAAccountSequence String @map("party_a_account_sequence") @db.VarChar(20) // 卖方 + partyBAccountSequence String @map("party_b_account_sequence") @db.VarChar(20) // 买方 + + status String @default("ACTIVE") @map("status") @db.VarChar(20) // ACTIVE / EXPIRED / CLOSED + openedAt DateTime @default(now()) @map("opened_at") + expiresAt DateTime @map("expires_at") // openedAt + 24h + closedAt DateTime? @map("closed_at") + + createdAt DateTime @default(now()) @map("created_at") + + @@index([partyAAccountSequence]) + @@index([partyBAccountSequence]) + @@index([status, expiresAt]) + @@map("share_transfer_windows") +} +``` + +#### ShareTransferRecord(积分股转让记录) + +```prisma +model ShareTransferRecord { + id String @id @default(uuid()) + windowId String @map("window_id") // 关联窗口 + transferOrderNo String @map("transfer_order_no") @db.VarChar(50) // 关联的树转让订单号 + + fromAccountSequence String @map("from_account_sequence") @db.VarChar(20) + toAccountSequence String @map("to_account_sequence") @db.VarChar(20) + amount Decimal @map("amount") @db.Decimal(30, 8) // Share 数量 + + status String @default("COMPLETED") @map("status") @db.VarChar(20) + createdAt DateTime @default(now()) @map("created_at") + + @@index([windowId]) + @@index([transferOrderNo]) + @@index([fromAccountSequence]) + @@index([toAccountSequence]) + @@map("share_transfer_records") +} +``` + +### 16.3 流程 + +``` +树转让完成 (TransferOrder.status = COMPLETED) + ↓ +transfer-service 发布 Outbox 事件: + → transfer.share-window.open + payload: { transferOrderNo, sellerAccountSeq, buyerAccountSeq } + ↓ +mining-wallet-service 消费事件: + → 创建 ShareTransferWindow (status=ACTIVE, expiresAt=now+24h) + ↓ +24小时内,双方通过 API 发起 Share 转让: + POST /share-transfers + { + transferOrderNo: "TRF260218A3B7C9D1", + toAccountSequence: "D25121400002", + amount: 1000.00 + } + ↓ +mining-wallet-service 处理: + 1. 查找 ACTIVE 状态的 ShareTransferWindow + 2. 验证发起方和接收方都在窗口的 partyA/partyB 中 + 3. 验证 expiresAt > now() + 4. 调用已有的 transferBetweenUsers() 执行 Share 转账 + 5. 创建 ShareTransferRecord 记录 + 6. 记录 UserWalletTransaction (transactionType=TRANSFER_OUT/TRANSFER_IN) + ↓ +24小时到期: + → 定时任务扫描 expiresAt < now() 且 status=ACTIVE 的窗口 + → 更新 status = EXPIRED +``` + +### 16.4 API 设计(mining-wallet-service 新增) + +``` +POST /share-transfers - 发起积分股转让 +GET /share-transfers/window/:orderNo - 查询窗口状态和剩余时间 +GET /share-transfers/records - 查询我的积分股转让记录 +``` + +#### POST /share-transfers 请求体 + +```json +{ + "transferOrderNo": "TRF260218A3B7C9D1", + "toAccountSequence": "D25121400002", + "amount": 1000.00000000 +} +``` + +#### POST /share-transfers 响应 + +```json +{ + "success": true, + "data": { + "recordId": "uuid", + "fromAccountSequence": "D25011500001", + "toAccountSequence": "D25121400002", + "amount": "1000.00000000", + "windowExpiresAt": "2026-02-19T14:30:00Z", + "createdAt": "2026-02-18T15:00:00Z" + } +} +``` + +#### GET /share-transfers/window/:orderNo 响应 + +```json +{ + "success": true, + "data": { + "transferOrderNo": "TRF260218A3B7C9D1", + "status": "ACTIVE", + "partyA": "D25011500001", + "partyB": "D25121400002", + "openedAt": "2026-02-18T14:30:00Z", + "expiresAt": "2026-02-19T14:30:00Z", + "remainingSeconds": 72000, + "records": [ + { "from": "D25011500001", "to": "D25121400002", "amount": "1000.00", "at": "..." } + ] + } +} +``` + +### 16.5 mining-wallet-service 新增文件 + +| 文件 | 说明 | +|------|------| +| `src/application/services/share-transfer.service.ts` | 积分股转让业务服务 | +| `src/application/event-handlers/share-window-opened.handler.ts` | 消费树转让完成事件,创建窗口 | +| `src/application/schedulers/share-window-expiry.scheduler.ts` | 定时任务:过期窗口关闭 | +| `src/api/controllers/share-transfer.controller.ts` | REST API | + +### 16.6 Mobile App 界面 + +树转让完成后,在转让详情页底部显示积分股转让入口: + +``` +┌──────────────────────────────┐ +│ 转让已完成 ✓ │ +│ │ +│ ┌────────────────────────┐ │ +│ │ 积分股转让窗口 │ │ +│ │ 剩余时间:18:45:30 │ │ +│ │ │ │ +│ │ 您的 Share:12,345.67 │ │ +│ │ 对方 Share:8,901.23 │ │ +│ │ │ │ +│ │ 转让数量:________ │ │ +│ │ │ │ +│ │ [转给对方] │ │ +│ └────────────────────────┘ │ +│ │ +│ 转让记录: │ +│ • 您 → 对方 1,000 Share │ +│ • 对方 → 您 500 Share │ +└──────────────────────────────┘ +``` + +### 16.7 实施归属 + +积分股转让功能归入 **Phase 2(算力调整)** 阶段,因为: +- 依赖树转让完成事件 +- 在 mining-wallet-service(2.0)中实现 +- 复用已有的 `transferBetweenUsers()` 方法 +- 只需新增窗口管理和验证逻辑 + +--- + +## 附录 A:配置项 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| TRANSFER_PLATFORM_FEE_RATE | 0.02 | 平台手续费率 2% | +| TRANSFER_MIN_PRICE_PER_TREE | 0 | 每棵树最低转让价 | +| TRANSFER_MAX_PRICE_PER_TREE | 99999 | 每棵树最高转让价 | +| TRANSFER_SAGA_STEP_TIMEOUT_MS | 300000 | Saga 单步超时 5 分钟 | +| TRANSFER_SAGA_MAX_RETRIES | 3 | Saga 步骤最大重试次数 | +| TRANSFER_ENABLED | true | 转让功能总开关 | +| TRANSFER_DAILY_LIMIT_PER_USER | 10 | 每用户每日转让次数上限 | +| TRANSFER_COOLDOWN_DAYS | 0 | 认种后 N 天内不可转让 | +| SHARE_TRANSFER_WINDOW_HOURS | 24 | 积分股转让窗口有效期(小时) | +| SHARE_TRANSFER_ENABLED | true | 积分股转让功能开关 | + +## 附录 B:新增 Kafka Topic 列表 + +| Topic | 生产者 | 消费者 | +|-------|--------|--------| +| `transfer.trees.lock` | transfer-service | planting-service | +| `transfer.trees.lock.ack` | planting-service | transfer-service | +| `transfer.ownership.execute` | transfer-service | planting-service | +| `transfer.trees.unlock` | transfer-service | planting-service | +| `transfer.trees.unlock.ack` | planting-service | transfer-service | +| `planting.transfer.completed` | planting-service | transfer-service | +| `planting.transfer.rolledback` | planting-service | transfer-service | +| `planting.ownership.removed` | planting-service | contribution-service, referral-service | +| `planting.ownership.added` | planting-service | contribution-service, referral-service | +| `contribution.transfer.adjusted` | contribution-service | transfer-service | +| `referral.transfer.stats-updated` | referral-service | transfer-service | +| `transfer.share-window.open` | transfer-service | mining-wallet-service | + +## 附录 C:新增数据库表汇总 + +| 服务 | 表名 | 说明 | +|------|------|------| +| transfer-service | transfer_orders | 转让订单(Saga 聚合根) | +| transfer-service | transfer_status_logs | 状态变更日志 | +| transfer-service | outbox_events | Outbox 事件 | +| transfer-service | processed_events | 幂等性检查 | +| planting-service | planting_transfer_records | 转让记录 | +| mining-wallet-service | share_transfer_windows | 积分股转让窗口(24小时有效期) | +| mining-wallet-service | share_transfer_records | 积分股转让记录 | + +## 附录 D:现有表新增字段汇总 + +| 服务 | 表名 | 新增字段 | 类型 | 说明 | +|------|------|---------|------|------| +| planting-service | planting_orders | transfer_locked_count | Int DEFAULT 0 | 被转让锁定的棵数 | +| contribution-service | contribution_records | transfer_order_no | VARCHAR(50) NULL | 关联转让订单号 | +| contribution-service | unallocated_contributions | transfer_order_no | VARCHAR(50) NULL | 关联转让订单号 | diff --git a/frontend/admin-web/src/app/(dashboard)/transfers/[transferOrderNo]/page.tsx b/frontend/admin-web/src/app/(dashboard)/transfers/[transferOrderNo]/page.tsx new file mode 100644 index 00000000..3643a0d2 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/transfers/[transferOrderNo]/page.tsx @@ -0,0 +1,337 @@ +'use client'; + +/** + * [2026-02-19] 树转让详情页(纯新增) + * + * 管理员查看单笔转让订单详情: + * - 基本信息卡片(卖方/买方/金额/手续费) + * - Saga 进度时间线(8 步可视化) + * - 状态变更日志表格 + * - 强制取消按钮(非终态订单) + * + * === 回滚方式 === + * 删除 transfers/ 目录即可 + */ + +import { useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from '@/components/common'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import { formatDateTime } from '@/utils/formatters'; +import { useTransferDetail, useForceCancel } from '@/hooks'; +import type { TransferOrderStatus } from '@/services/transferService'; +import styles from '../transfers.module.scss'; + +// Saga 步骤顺序 +const SAGA_STEPS: { status: TransferOrderStatus; label: string }[] = [ + { status: 'PENDING', label: '待卖方确认' }, + { status: 'SELLER_CONFIRMED', label: '卖方已确认' }, + { status: 'PAYMENT_FROZEN', label: '买方资金冻结' }, + { status: 'TREES_LOCKED', label: '卖方树锁定' }, + { status: 'OWNERSHIP_TRANSFERRED', label: '所有权变更' }, + { status: 'CONTRIBUTION_ADJUSTED', label: '算力调整' }, + { status: 'STATS_UPDATED', label: '团队统计更新' }, + { status: 'PAYMENT_SETTLED', label: '资金结算' }, + { status: 'COMPLETED', label: '转让完成' }, +]; + +// 状态标签映射 +const STATUS_LABEL: Record = { + PENDING: '待确认', + SELLER_CONFIRMED: '卖方已确认', + PAYMENT_FROZEN: '资金已冻结', + TREES_LOCKED: '树已锁定', + OWNERSHIP_TRANSFERRED: '所有权变更', + CONTRIBUTION_ADJUSTED: '算力调整', + STATS_UPDATED: '统计更新', + PAYMENT_SETTLED: '资金结算', + COMPLETED: '已完成', + CANCELLED: '已取消', + FAILED: '失败', + ROLLING_BACK: '补偿中', + ROLLED_BACK: '已回滚', +}; + +// 终态判断 +const TERMINAL_STATUSES = ['COMPLETED', 'CANCELLED', 'ROLLED_BACK']; + +/** + * 转让详情页 + */ +export default function TransferDetailPage() { + const params = useParams(); + const router = useRouter(); + const transferOrderNo = params.transferOrderNo as string; + + const { data: order, isLoading, error, refetch } = useTransferDetail(transferOrderNo); + const forceCancel = useForceCancel(); + const [cancelReason, setCancelReason] = useState(''); + const [showCancelConfirm, setShowCancelConfirm] = useState(false); + + const handleForceCancel = () => { + if (!cancelReason.trim()) return; + forceCancel.mutate( + { transferOrderNo, reason: cancelReason }, + { + onSuccess: () => { + setShowCancelConfirm(false); + setCancelReason(''); + refetch(); + }, + }, + ); + }; + + if (isLoading) { + return ( + +
加载中...
+
+ ); + } + + if (error || !order) { + return ( + +
+ {error?.message || '订单不存在'} + +
+
+ ); + } + + const isTerminal = TERMINAL_STATUSES.includes(order.status); + const currentStepIndex = SAGA_STEPS.findIndex((s) => s.status === order.status); + const isFailed = order.status === 'FAILED' || order.status === 'ROLLING_BACK' || order.status === 'ROLLED_BACK'; + + return ( + +
+ {/* 返回按钮 */} + + + {/* 页面标题 */} +
+

+ 转让单:{order.transferOrderNo} + + {STATUS_LABEL[order.status] || order.status} + +

+
+ + {/* 基本信息卡片 */} +
+
+
+ 卖方账号 + {order.sellerAccountSequence} +
+
+ 买方账号 + {order.buyerAccountSequence} +
+
+ 转让棵数 + {order.treeCount} 棵 +
+
+ 单价 + {Number(order.pricePerTree).toLocaleString()} USDT +
+
+ 总价 + {Number(order.totalPrice).toLocaleString()} USDT +
+
+ 手续费率 + {(Number(order.platformFeeRate) * 100).toFixed(1)}% +
+
+ 手续费 + {Number(order.platformFeeAmount).toLocaleString()} USDT +
+
+ 卖方到账 + {Number(order.sellerReceiveAmount).toLocaleString()} USDT +
+
+ 创建时间 + {formatDateTime(order.createdAt)} +
+ {order.completedAt && ( +
+ 完成时间 + {formatDateTime(order.completedAt)} +
+ )} + {order.cancelledAt && ( +
+ 取消时间 + {formatDateTime(order.cancelledAt)} +
+ )} + {order.cancelReason && ( +
+ 取消原因 + {order.cancelReason} +
+ )} +
+
+ + {/* Saga 进度时间线 */} +
+

Saga 进度

+
+
+ {SAGA_STEPS.map((step, idx) => { + let dotStyle = ''; + if (isFailed) { + dotStyle = idx <= currentStepIndex ? 'completed' : ''; + if (idx === currentStepIndex) dotStyle = 'failed'; + } else if (idx < currentStepIndex || (isTerminal && idx <= currentStepIndex)) { + dotStyle = 'completed'; + } else if (idx === currentStepIndex) { + dotStyle = 'current'; + } + + // 从状态日志中查找对应时间 + const logEntry = order.statusLogs?.find((l) => l.toStatus === step.status); + + return ( +
+
+ {dotStyle === 'completed' ? '\u2713' : dotStyle === 'failed' ? '\u2717' : ''} +
+
+
{step.label}
+ {logEntry && ( +
+ {formatDateTime(logEntry.createdAt)} +
+ )} +
+
+ ); + })} +
+
+
+ + {/* 状态变更日志 */} + {order.statusLogs && order.statusLogs.length > 0 && ( +
+

状态变更日志

+
+ + + + + + + + + + + {order.statusLogs.map((log) => ( + + + + + + + ))} + +
从状态到状态原因时间
{STATUS_LABEL[log.fromStatus] || log.fromStatus}{STATUS_LABEL[log.toStatus] || log.toStatus}{log.reason || '-'}{formatDateTime(log.createdAt)}
+
+
+ )} + + {/* 强制取消操作 */} + {!isTerminal && ( +
+ {!showCancelConfirm ? ( + + ) : ( +
+ setCancelReason(e.target.value)} + style={{ + padding: '8px 12px', + border: '1px solid #ff4d4f', + borderRadius: 8, + fontSize: 14, + width: 280, + }} + /> + + +
+ )} +
+ )} +
+
+ ); +} + +/** + * 状态名转样式类后缀 + */ +function statusToStyle(status: string): string { + const map: Record = { + PENDING: 'pending', + SELLER_CONFIRMED: 'sellerConfirmed', + PAYMENT_FROZEN: 'paymentFrozen', + TREES_LOCKED: 'treesLocked', + OWNERSHIP_TRANSFERRED: 'ownershipTransferred', + CONTRIBUTION_ADJUSTED: 'contributionAdjusted', + STATS_UPDATED: 'statsUpdated', + PAYMENT_SETTLED: 'paymentSettled', + COMPLETED: 'completed', + CANCELLED: 'cancelled', + FAILED: 'failed', + ROLLING_BACK: 'rollingBack', + ROLLED_BACK: 'rolledBack', + }; + return map[status] || ''; +} diff --git a/frontend/admin-web/src/app/(dashboard)/transfers/page.tsx b/frontend/admin-web/src/app/(dashboard)/transfers/page.tsx new file mode 100644 index 00000000..0fbe10fe --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/transfers/page.tsx @@ -0,0 +1,266 @@ +'use client'; + +/** + * [2026-02-19] 树转让管理列表页(纯新增) + * + * 管理员端的树转让管理页面,提供: + * - 统计汇总(总数/待处理/已完成/已取消/失败/总成交额) + * - 关键字搜索 + 状态筛选 + 分页 + * - 转让订单表格(点击订单号跳详情) + * - 13 种 Saga 状态 badge 颜色映射 + * + * === 回滚方式 === + * 删除 transfers/ 目录即可 + */ + +import { useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/common'; +import { PageContainer } from '@/components/layout'; +import { cn } from '@/utils/helpers'; +import { formatDateTime } from '@/utils/formatters'; +import { useTransferStats, useTransferList } from '@/hooks'; +import type { TransferOrder, TransferOrderStatus } from '@/services/transferService'; +import styles from './transfers.module.scss'; + +// 状态显示映射(13 种) +const STATUS_MAP: Record = { + PENDING: { label: '待确认', style: 'pending' }, + SELLER_CONFIRMED: { label: '卖方已确认', style: 'sellerConfirmed' }, + PAYMENT_FROZEN: { label: '资金已冻结', style: 'paymentFrozen' }, + TREES_LOCKED: { label: '树已锁定', style: 'treesLocked' }, + OWNERSHIP_TRANSFERRED: { label: '所有权变更', style: 'ownershipTransferred' }, + CONTRIBUTION_ADJUSTED: { label: '算力调整', style: 'contributionAdjusted' }, + STATS_UPDATED: { label: '统计更新', style: 'statsUpdated' }, + PAYMENT_SETTLED: { label: '资金结算', style: 'paymentSettled' }, + COMPLETED: { label: '已完成', style: 'completed' }, + CANCELLED: { label: '已取消', style: 'cancelled' }, + FAILED: { label: '失败', style: 'failed' }, + ROLLING_BACK: { label: '补偿中', style: 'rollingBack' }, + ROLLED_BACK: { label: '已回滚', style: 'rolledBack' }, +}; + +// 状态筛选选项 +const STATUS_OPTIONS: { value: TransferOrderStatus | ''; label: string }[] = [ + { value: '', label: '全部状态' }, + { value: 'PENDING', label: '待确认' }, + { value: 'SELLER_CONFIRMED', label: '卖方已确认' }, + { value: 'PAYMENT_FROZEN', label: '资金已冻结' }, + { value: 'COMPLETED', label: '已完成' }, + { value: 'CANCELLED', label: '已取消' }, + { value: 'FAILED', label: '失败' }, + { value: 'ROLLING_BACK', label: '补偿中' }, + { value: 'ROLLED_BACK', label: '已回滚' }, +]; + +/** + * 转让管理列表页 + */ +export default function TransfersPage() { + const [keyword, setKeyword] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [page, setPage] = useState(1); + const pageSize = 20; + + const { data: stats, isLoading: statsLoading } = useTransferStats(); + const listQuery = useTransferList({ + page, + pageSize, + keyword: keyword || undefined, + status: statusFilter || undefined, + }); + + const formatNumber = (n: number) => n.toLocaleString('en-US'); + + const totalPages = listQuery.data ? Math.ceil(listQuery.data.total / pageSize) : 0; + + return ( + +
+ {/* 页面标题 */} +
+

树转让管理

+
+ + {/* 统计卡片 */} +
+
+
+ {statsLoading ? '-' : formatNumber(stats?.totalOrders ?? 0)} +
+
总订单数
+
+
+
+ {statsLoading ? '-' : formatNumber(stats?.pendingOrders ?? 0)} +
+
待处理
+
+
+
+ {statsLoading ? '-' : formatNumber(stats?.completedOrders ?? 0)} +
+
已完成
+
+
+
+ {statsLoading ? '-' : formatNumber(stats?.cancelledOrders ?? 0)} +
+
已取消
+
+
+
+ {statsLoading ? '-' : formatNumber(stats?.totalVolume ?? 0)} +
+
总成交额 (USDT)
+
+
+ + {/* 数据区 */} +
+ {/* 工具栏:搜索 + 筛选 + 刷新 */} +
+
+
+ { + setKeyword(e.target.value); + setPage(1); + }} + /> +
+
+ +
+
+ +
+ + {/* 表格 */} + listQuery.refetch()} + /> + + {/* 分页 */} + {totalPages > 1 && ( +
+ + + {page} / {totalPages} + + +
+ )} +
+
+
+ ); +} + +// ============================================ +// 子组件:转让订单表格 +// ============================================ + +function TransfersTable({ + data, + loading, + error, + onRetry, +}: { + data: TransferOrder[]; + loading: boolean; + error: Error | null; + onRetry: () => void; +}) { + if (loading) return
加载中...
; + if (error) { + return ( +
+ {error.message || '加载失败'} + +
+ ); + } + if (data.length === 0) return
暂无转让订单
; + + return ( + + + + + + + + + + + + + + + {data.map((order) => { + const status = STATUS_MAP[order.status] ?? { label: order.status, style: '' }; + return ( + + + + + + + + + + + ); + })} + +
转让单号卖方买方棵数单价 (USDT)总价 (USDT)状态创建时间
+ + {order.transferOrderNo} + + {order.sellerAccountSequence}{order.buyerAccountSequence}{order.treeCount}{Number(order.pricePerTree).toLocaleString()}{Number(order.totalPrice).toLocaleString()} + + {status.label} + + {formatDateTime(order.createdAt)}
+ ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/transfers/transfers.module.scss b/frontend/admin-web/src/app/(dashboard)/transfers/transfers.module.scss new file mode 100644 index 00000000..46f5def3 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/transfers/transfers.module.scss @@ -0,0 +1,453 @@ +/** + * [2026-02-19] 树转让管理页面样式(纯新增) + * + * 包含:统计卡片、状态筛选、数据表格、详情页、Saga 进度时间线 + * 风格与预种管理页面一致 + * + * 回滚方式:删除此文件 + */ + +@use '@/styles/variables' as *; + +.transfers { + display: flex; + flex-direction: column; + gap: 24px; + + // 页面标题 + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__title { + font-size: 24px; + font-weight: 600; + color: $text-primary; + } + + // 统计卡片网格 + &__statsGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + + @media (max-width: 1400px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (max-width: 900px) { + grid-template-columns: repeat(2, 1fr); + } + } + + &__statCard { + background: $card-background; + border-radius: 12px; + padding: 20px; + box-shadow: $shadow-base; + text-align: center; + } + + &__statValue { + font-size: 28px; + font-weight: 700; + color: $text-primary; + } + + &__statLabel { + font-size: 13px; + color: $text-secondary; + margin-top: 4px; + } + + // 数据卡片容器 + &__card { + background: $card-background; + border-radius: 12px; + padding: 24px; + box-shadow: $shadow-base; + } + + &__toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + gap: 12px; + flex-wrap: wrap; + } + + &__toolbarLeft { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; + } + + &__search { + input { + width: 260px; + padding: 8px 12px; + border: 1px solid $border-color; + border-radius: 8px; + font-size: 14px; + outline: none; + + &:focus { + border-color: #d4af37; + } + } + } + + &__filter { + select { + padding: 8px 12px; + border: 1px solid $border-color; + border-radius: 8px; + font-size: 14px; + outline: none; + background: white; + cursor: pointer; + + &:focus { + border-color: #d4af37; + } + } + } + + // 表格 + &__table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid $border-color; + font-size: 14px; + } + + th { + font-weight: 600; + color: $text-secondary; + background: #fafafa; + } + + td { + color: $text-primary; + } + + tr:hover td { + background: #fafafa; + } + } + + &__orderLink { + color: #d4af37; + text-decoration: none; + font-weight: 500; + + &:hover { + text-decoration: underline; + } + } + + // 状态标签(13 种状态) + &__status { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + + // 进行中状态 + &--pending { + background: #fff7e6; + color: #d48806; + } + + &--sellerConfirmed { + background: #e6f7ff; + color: #096dd9; + } + + &--paymentFrozen { + background: #f0f5ff; + color: #2f54eb; + } + + &--treesLocked { + background: #f9f0ff; + color: #722ed1; + } + + &--ownershipTransferred { + background: #fff0f6; + color: #c41d7f; + } + + &--contributionAdjusted { + background: #fcffe6; + color: #7cb305; + } + + &--statsUpdated { + background: #e6fffb; + color: #08979c; + } + + &--paymentSettled { + background: #f6ffed; + color: #389e0d; + } + + // 终态 + &--completed { + background: #f6ffed; + color: #52c41a; + } + + &--cancelled { + background: #f5f5f5; + color: #8c8c8c; + } + + &--failed { + background: #fff1f0; + color: #cf1322; + } + + &--rollingBack { + background: #fff2e8; + color: #d4380d; + } + + &--rolledBack { + background: #f5f5f5; + color: #595959; + } + } + + // 加载 / 空 / 错误 状态 + &__loading, + &__empty, + &__error { + text-align: center; + padding: 48px 24px; + color: $text-secondary; + font-size: 14px; + } + + &__error { + color: #ff4d4f; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + } + + // 分页 + &__pagination { + display: flex; + justify-content: flex-end; + margin-top: 16px; + gap: 8px; + align-items: center; + } + + &__pageBtn { + padding: 6px 12px; + border: 1px solid $border-color; + border-radius: 6px; + background: white; + cursor: pointer; + font-size: 13px; + + &:hover { + border-color: #d4af37; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } + + &__pageInfo { + font-size: 13px; + color: $text-secondary; + } + + // === 详情页样式 === + &__backBtn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border: 1px solid $border-color; + border-radius: 8px; + background: white; + cursor: pointer; + font-size: 14px; + color: $text-secondary; + + &:hover { + border-color: #d4af37; + color: $text-primary; + } + } + + &__detailGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 24px; + + @media (max-width: 900px) { + grid-template-columns: repeat(2, 1fr); + } + } + + &__detailItem { + display: flex; + flex-direction: column; + gap: 4px; + } + + &__detailLabel { + font-size: 12px; + color: $text-secondary; + } + + &__detailValue { + font-size: 15px; + font-weight: 500; + color: $text-primary; + } + + &__section { + margin-top: 24px; + } + + &__sectionTitle { + font-size: 16px; + font-weight: 600; + color: $text-primary; + margin-bottom: 16px; + } + + // Saga 进度时间线 + &__timeline { + display: flex; + flex-direction: column; + gap: 0; + margin-bottom: 24px; + } + + &__timelineStep { + display: flex; + align-items: flex-start; + gap: 12px; + position: relative; + padding-bottom: 24px; + + &:last-child { + padding-bottom: 0; + } + + // 竖线连接 + &::before { + content: ''; + position: absolute; + left: 11px; + top: 24px; + bottom: 0; + width: 2px; + background: $border-color; + } + + &:last-child::before { + display: none; + } + } + + &__timelineDot { + width: 24px; + height: 24px; + border-radius: 50%; + border: 2px solid $border-color; + background: white; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + z-index: 1; + + &--completed { + border-color: #52c41a; + background: #52c41a; + color: white; + } + + &--current { + border-color: #d4af37; + background: #fff7e6; + color: #d4af37; + } + + &--failed { + border-color: #ff4d4f; + background: #ff4d4f; + color: white; + } + } + + &__timelineContent { + flex: 1; + min-width: 0; + } + + &__timelineLabel { + font-size: 14px; + font-weight: 500; + color: $text-primary; + } + + &__timelineTime { + font-size: 12px; + color: $text-secondary; + margin-top: 2px; + } + + // 操作按钮区 + &__actions { + display: flex; + gap: 12px; + margin-top: 24px; + } + + &__cancelBtn { + padding: 8px 20px; + border-radius: 8px; + border: 1px solid #ff4d4f; + background: white; + color: #ff4d4f; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #fff1f0; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} diff --git a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx index cbd45c5e..05ab1bf2 100644 --- a/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/admin-web/src/components/layout/Sidebar/Sidebar.tsx @@ -37,6 +37,8 @@ const topMenuItems: MenuItem[] = [ { key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' }, // [2026-02-17] 新增:预种计划管理(3171 USDT/份预种开关 + 订单/持仓/合并查询) { key: 'pre-planting', icon: '/images/Container3.svg', label: '预种管理', path: '/pre-planting' }, + // [2026-02-19] 纯新增:树转让管理 + { key: 'transfers', icon: '/images/Container5.svg', label: '转让管理', path: '/transfers' }, { key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' }, { key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' }, ]; diff --git a/frontend/admin-web/src/hooks/index.ts b/frontend/admin-web/src/hooks/index.ts index 73975a93..8253e5ba 100644 --- a/frontend/admin-web/src/hooks/index.ts +++ b/frontend/admin-web/src/hooks/index.ts @@ -7,3 +7,5 @@ export * from './useAuthorizations'; export * from './useSystemWithdrawal'; // [2026-02-17] 预种计划管理 export * from './usePrePlanting'; +// [2026-02-19] 树转让管理 +export * from './useTransfers'; diff --git a/frontend/admin-web/src/hooks/useTransfers.ts b/frontend/admin-web/src/hooks/useTransfers.ts new file mode 100644 index 00000000..4dacf921 --- /dev/null +++ b/frontend/admin-web/src/hooks/useTransfers.ts @@ -0,0 +1,80 @@ +/** + * [2026-02-19] 树转让管理 React Query Hooks(纯新增) + * + * 为 admin-web 转让管理页面提供数据获取 hooks。 + * 复用项目的 React Query 模式(Query key factory + hooks)。 + * + * === 回滚方式 === + * 删除此文件并从 hooks/index.ts 移除 export + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + transferService, + type TransferListParams, +} from '@/services/transferService'; + +// ============================================ +// Query Key Factory +// ============================================ + +export const transferKeys = { + all: ['transfers'] as const, + stats: () => [...transferKeys.all, 'stats'] as const, + list: (params: TransferListParams) => + [...transferKeys.all, 'list', params] as const, + detail: (transferOrderNo: string) => + [...transferKeys.all, 'detail', transferOrderNo] as const, +}; + +// ============================================ +// Query Hooks +// ============================================ + +/** 获取转让统计汇总 */ +export function useTransferStats() { + return useQuery({ + queryKey: transferKeys.stats(), + queryFn: () => transferService.getStats(), + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + }); +} + +/** 获取转让订单列表 */ +export function useTransferList(params: TransferListParams = {}) { + return useQuery({ + queryKey: transferKeys.list(params), + queryFn: () => transferService.getList(params), + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + }); +} + +/** 获取转让订单详情 */ +export function useTransferDetail(transferOrderNo: string) { + return useQuery({ + queryKey: transferKeys.detail(transferOrderNo), + queryFn: () => transferService.getDetail(transferOrderNo), + enabled: !!transferOrderNo, + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + }); +} + +// ============================================ +// Mutation Hooks +// ============================================ + +/** 强制取消转让订单 */ +export function useForceCancel() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ transferOrderNo, reason }: { transferOrderNo: string; reason: string }) => + transferService.forceCancel(transferOrderNo, reason), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: transferKeys.all }); + }, + }); +} diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index 6af9dab4..cb416e1c 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -304,4 +304,12 @@ export const API_ENDPOINTS = { // 预种统计汇总(总份数、总金额、合并树数等) STATS: '/v1/admin/pre-planting/stats', }, + // [2026-02-19] 纯新增:树转让管理 (transfer-service) + // Saga 编排的树转让订单管理:列表/详情/统计/强制取消 + TRANSFERS: { + LIST: '/v1/admin/transfers', + DETAIL: (no: string) => `/v1/admin/transfers/${no}`, + STATS: '/v1/admin/transfers/stats', + FORCE_CANCEL: (no: string) => `/v1/admin/transfers/${no}/force-cancel`, + }, } as const; diff --git a/frontend/admin-web/src/services/transferService.ts b/frontend/admin-web/src/services/transferService.ts new file mode 100644 index 00000000..701c0347 --- /dev/null +++ b/frontend/admin-web/src/services/transferService.ts @@ -0,0 +1,139 @@ +/** + * [2026-02-19] 树转让管理服务(纯新增) + * + * 管理员端的树转让 API 调用服务。 + * 提供转让订单列表/详情/统计/强制取消等功能。 + * + * === API 端点 === + * 所有端点走 transfer-service 的 AdminController: + * - GET /v1/admin/transfers 转让订单列表 + * - GET /v1/admin/transfers/:no 转让订单详情 + * - GET /v1/admin/transfers/stats 转让统计汇总 + * - POST /v1/admin/transfers/:no/force-cancel 强制取消 + * + * === 回滚方式 === + * 删除此文件 + endpoints.ts TRANSFERS 段 + hooks + 页面文件 + Sidebar 入口 + */ + +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; + +// ============================================ +// 类型定义 +// ============================================ + +/** 转让订单状态(13 种) */ +export type TransferOrderStatus = + | 'PENDING' + | 'SELLER_CONFIRMED' + | 'PAYMENT_FROZEN' + | 'TREES_LOCKED' + | 'OWNERSHIP_TRANSFERRED' + | 'CONTRIBUTION_ADJUSTED' + | 'STATS_UPDATED' + | 'PAYMENT_SETTLED' + | 'COMPLETED' + | 'CANCELLED' + | 'FAILED' + | 'ROLLING_BACK' + | 'ROLLED_BACK'; + +/** 转让统计汇总 */ +export interface TransferStats { + totalOrders: number; + pendingOrders: number; + completedOrders: number; + cancelledOrders: number; + failedOrders: number; + totalTreesTransferred: number; + totalVolume: number; // 总交易额 USDT + totalFees: number; // 总手续费 USDT +} + +/** 转让订单(管理员视角列表) */ +export interface TransferOrder { + id: string; + transferOrderNo: string; + sellerAccountSequence: string; + buyerAccountSequence: string; + treeCount: number; + pricePerTree: string; + totalPrice: string; + platformFeeRate: string; + platformFeeAmount: string; + sellerReceiveAmount: string; + status: TransferOrderStatus; + createdAt: string; + updatedAt: string; + completedAt: string | null; + cancelledAt: string | null; + cancelReason: string | null; +} + +/** 转让状态变更日志 */ +export interface TransferStatusLog { + id: string; + fromStatus: string; + toStatus: string; + reason: string | null; + createdAt: string; +} + +/** 转让订单详情(含状态日志) */ +export interface TransferOrderDetail extends TransferOrder { + statusLogs: TransferStatusLog[]; + plantingOrderNos: string[]; + freezeId: string | null; +} + +/** 分页列表请求参数 */ +export interface TransferListParams { + page?: number; + pageSize?: number; + status?: TransferOrderStatus | ''; + keyword?: string; +} + +/** 分页列表响应 */ +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; +} + +// ============================================ +// 转让管理服务 +// ============================================ + +export const transferService = { + /** + * 获取转让订单列表 + */ + async getList(params: TransferListParams = {}): Promise> { + return apiClient.get(API_ENDPOINTS.TRANSFERS.LIST, { params }); + }, + + /** + * 获取转让订单详情(含状态变更日志) + */ + async getDetail(transferOrderNo: string): Promise { + return apiClient.get(API_ENDPOINTS.TRANSFERS.DETAIL(transferOrderNo)); + }, + + /** + * 获取转让统计汇总 + */ + async getStats(): Promise { + return apiClient.get(API_ENDPOINTS.TRANSFERS.STATS); + }, + + /** + * 强制取消转让订单(管理员权限) + */ + async forceCancel(transferOrderNo: string, reason: string): Promise<{ success: boolean }> { + return apiClient.post(API_ENDPOINTS.TRANSFERS.FORCE_CANCEL(transferOrderNo), { reason }); + }, +}; + +export default transferService; diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index 8fff87c9..5adb280a 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -11,6 +11,8 @@ import '../services/wallet_service.dart'; import '../services/planting_service.dart'; // [2026-02-17] 新增:预种计划服务(3171 USDT/份,独立于现有认种) import '../services/pre_planting_service.dart'; +// [2026-02-19] 纯新增:树转让服务(已认种树所有权转让) +import '../services/transfer_service.dart'; import '../services/reward_service.dart'; import '../services/notification_service.dart'; import '../services/system_config_service.dart'; @@ -104,6 +106,13 @@ final prePlantingServiceProvider = Provider((ref) { return PrePlantingService(apiClient: apiClient); }); +// [2026-02-19] Transfer Service Provider (调用 transfer-service) +// 树转让:已认种树所有权在用户间转让。与上方 PlantingService 完全独立。 +final transferServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + return TransferService(apiClient: apiClient); +}); + // Reward Service Provider (直接调用 reward-service) final rewardServiceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); diff --git a/frontend/mobile-app/lib/core/services/transfer_service.dart b/frontend/mobile-app/lib/core/services/transfer_service.dart new file mode 100644 index 00000000..4cb8c36e --- /dev/null +++ b/frontend/mobile-app/lib/core/services/transfer_service.dart @@ -0,0 +1,358 @@ +import 'package:flutter/foundation.dart'; +import '../network/api_client.dart'; + +// ============================================ +// [2026-02-19] 树转让 API 服务(纯新增) +// ============================================ +// +// 树转让功能的 Flutter 端 API 调用服务。 +// 用户可将已认种的果树转让给其他用户。 +// +// === API 端点 === +// 所有端点走 transfer-service: +// - POST /transfers 发起转让 +// - POST /transfers/:no/confirm 卖方确认 +// - POST /transfers/:no/cancel 取消转让 +// - GET /transfers/my 我的转让记录 +// - GET /transfers/:no 转让详情 +// +// === 与现有 PlantingService 的关系 === +// 完全独立。PlantingService 处理认种购买, +// TransferService 处理已认种树的所有权转让。 +// +// === 回滚方式 === +// 删除此文件 + DI 注册 + 路由 + 页面文件 + profile 入口 + +/// 转让订单状态(13 种 Saga 步骤) +enum TransferOrderStatus { + pending, // 待卖方确认 + sellerConfirmed, // 卖方已确认 + paymentFrozen, // 买方资金已冻结 + treesLocked, // 卖方树已锁定 + ownershipTransferred, // 所有权已变更 + contributionAdjusted, // 算力已调整 + statsUpdated, // 团队统计已更新 + paymentSettled, // 资金已结算 + completed, // 转让完成 + cancelled, // 已取消 + failed, // 失败 + rollingBack, // 补偿中 + rolledBack, // 已回滚 +} + +/// 转让订单 +class TransferOrder { + final String transferOrderNo; + final String sellerAccountSequence; + final String buyerAccountSequence; + final int treeCount; + final double pricePerTree; + final double totalPrice; + final double platformFeeRate; + final double platformFeeAmount; + final double sellerReceiveAmount; + final TransferOrderStatus status; + final DateTime createdAt; + final DateTime? completedAt; + final DateTime? cancelledAt; + final String? cancelReason; + + TransferOrder({ + required this.transferOrderNo, + required this.sellerAccountSequence, + required this.buyerAccountSequence, + required this.treeCount, + required this.pricePerTree, + required this.totalPrice, + required this.platformFeeRate, + required this.platformFeeAmount, + required this.sellerReceiveAmount, + required this.status, + required this.createdAt, + this.completedAt, + this.cancelledAt, + this.cancelReason, + }); + + /// 是否为终态 + bool get isTerminal => + status == TransferOrderStatus.completed || + status == TransferOrderStatus.cancelled || + status == TransferOrderStatus.rolledBack; + + /// 是否可取消(PENDING 或 SELLER_CONFIRMED 阶段) + bool get isCancellable => + status == TransferOrderStatus.pending || + status == TransferOrderStatus.sellerConfirmed; + + /// 是否待卖方确认 + bool get isPendingSellerConfirm => + status == TransferOrderStatus.pending; + + factory TransferOrder.fromJson(Map json) { + return TransferOrder( + transferOrderNo: json['transferOrderNo'] ?? '', + sellerAccountSequence: json['sellerAccountSequence'] ?? '', + buyerAccountSequence: json['buyerAccountSequence'] ?? '', + treeCount: json['treeCount'] ?? 0, + pricePerTree: _toDouble(json['pricePerTree']), + totalPrice: _toDouble(json['totalPrice']), + platformFeeRate: _toDouble(json['platformFeeRate']), + platformFeeAmount: _toDouble(json['platformFeeAmount']), + sellerReceiveAmount: _toDouble(json['sellerReceiveAmount']), + status: _parseStatus(json['status']), + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + completedAt: json['completedAt'] != null + ? DateTime.parse(json['completedAt']) + : null, + cancelledAt: json['cancelledAt'] != null + ? DateTime.parse(json['cancelledAt']) + : null, + cancelReason: json['cancelReason'], + ); + } + + static double _toDouble(dynamic value) { + if (value == null) return 0.0; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0.0; + return 0.0; + } + + static TransferOrderStatus _parseStatus(String? status) { + switch (status) { + case 'PENDING': return TransferOrderStatus.pending; + case 'SELLER_CONFIRMED': return TransferOrderStatus.sellerConfirmed; + case 'PAYMENT_FROZEN': return TransferOrderStatus.paymentFrozen; + case 'TREES_LOCKED': return TransferOrderStatus.treesLocked; + case 'OWNERSHIP_TRANSFERRED': return TransferOrderStatus.ownershipTransferred; + case 'CONTRIBUTION_ADJUSTED': return TransferOrderStatus.contributionAdjusted; + case 'STATS_UPDATED': return TransferOrderStatus.statsUpdated; + case 'PAYMENT_SETTLED': return TransferOrderStatus.paymentSettled; + case 'COMPLETED': return TransferOrderStatus.completed; + case 'CANCELLED': return TransferOrderStatus.cancelled; + case 'FAILED': return TransferOrderStatus.failed; + case 'ROLLING_BACK': return TransferOrderStatus.rollingBack; + case 'ROLLED_BACK': return TransferOrderStatus.rolledBack; + default: return TransferOrderStatus.pending; + } + } +} + +/// 转让状态变更日志 +class TransferStatusLog { + final String fromStatus; + final String toStatus; + final String? reason; + final DateTime createdAt; + + TransferStatusLog({ + required this.fromStatus, + required this.toStatus, + this.reason, + required this.createdAt, + }); + + factory TransferStatusLog.fromJson(Map json) { + return TransferStatusLog( + fromStatus: json['fromStatus'] ?? '', + toStatus: json['toStatus'] ?? '', + reason: json['reason'], + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + ); + } +} + +/// 转让订单详情(含状态日志) +class TransferOrderDetail extends TransferOrder { + final List statusLogs; + final List plantingOrderNos; + + TransferOrderDetail({ + required super.transferOrderNo, + required super.sellerAccountSequence, + required super.buyerAccountSequence, + required super.treeCount, + required super.pricePerTree, + required super.totalPrice, + required super.platformFeeRate, + required super.platformFeeAmount, + required super.sellerReceiveAmount, + required super.status, + required super.createdAt, + super.completedAt, + super.cancelledAt, + super.cancelReason, + required this.statusLogs, + required this.plantingOrderNos, + }); + + factory TransferOrderDetail.fromJson(Map json) { + final base = TransferOrder.fromJson(json); + return TransferOrderDetail( + transferOrderNo: base.transferOrderNo, + sellerAccountSequence: base.sellerAccountSequence, + buyerAccountSequence: base.buyerAccountSequence, + treeCount: base.treeCount, + pricePerTree: base.pricePerTree, + totalPrice: base.totalPrice, + platformFeeRate: base.platformFeeRate, + platformFeeAmount: base.platformFeeAmount, + sellerReceiveAmount: base.sellerReceiveAmount, + status: base.status, + createdAt: base.createdAt, + completedAt: base.completedAt, + cancelledAt: base.cancelledAt, + cancelReason: base.cancelReason, + statusLogs: (json['statusLogs'] as List?) + ?.map((e) => TransferStatusLog.fromJson(e as Map)) + .toList() ?? + [], + plantingOrderNos: (json['plantingOrderNos'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + [], + ); + } +} + +// ============================================ +// 树转让 API 服务 +// ============================================ + +/// 树转让 API 服务 +/// +/// [2026-02-19] 纯新增:提供树转让功能的所有 API 调用 +class TransferService { + final ApiClient _apiClient; + + TransferService({required ApiClient apiClient}) : _apiClient = apiClient; + + /// 发起转让 + /// + /// [buyerAccountSequence] 买方账号 + /// [plantingOrderNos] 要转让的认种订单号列表 + /// [pricePerTree] 每棵售价 + Future createTransfer({ + required String buyerAccountSequence, + required List plantingOrderNos, + required double pricePerTree, + }) async { + try { + debugPrint('[TransferService] 发起转让: buyer=$buyerAccountSequence, trees=${plantingOrderNos.length}'); + final response = await _apiClient.post( + '/transfers', + data: { + 'buyerAccountSequence': buyerAccountSequence, + 'plantingOrderNos': plantingOrderNos, + 'pricePerTree': pricePerTree.toString(), + }, + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[TransferService] 转让创建成功: ${data['transferOrderNo']}'); + return TransferOrder.fromJson(data); + } + + throw Exception('发起转让失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[TransferService] 发起转让失败: $e'); + rethrow; + } + } + + /// 卖方确认转让 + Future sellerConfirm(String transferOrderNo) async { + try { + debugPrint('[TransferService] 卖方确认: $transferOrderNo'); + final response = await _apiClient.post('/transfers/$transferOrderNo/confirm'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return TransferOrder.fromJson(data); + } + + throw Exception('确认转让失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[TransferService] 确认转让失败: $e'); + rethrow; + } + } + + /// 取消转让 + Future cancelTransfer(String transferOrderNo, {String? reason}) async { + try { + debugPrint('[TransferService] 取消转让: $transferOrderNo'); + await _apiClient.post( + '/transfers/$transferOrderNo/cancel', + data: {'reason': reason ?? '用户主动取消'}, + ); + debugPrint('[TransferService] 取消成功'); + } catch (e) { + debugPrint('[TransferService] 取消转让失败: $e'); + rethrow; + } + } + + /// 获取我的转让记录(买方+卖方) + Future> getMyTransfers({ + int page = 1, + int pageSize = 20, + String? role, // 'seller' | 'buyer' | null(全部) + }) async { + try { + debugPrint('[TransferService] 获取转让记录: role=$role'); + final response = await _apiClient.get( + '/transfers/my', + queryParameters: { + 'page': page, + 'pageSize': pageSize, + if (role != null) 'role': role, + }, + ); + + if (response.statusCode == 200) { + List items; + if (response.data is List) { + items = response.data as List; + } else if (response.data is Map) { + items = (response.data as Map)['items'] as List? ?? []; + } else { + items = []; + } + return items + .map((e) => TransferOrder.fromJson(e as Map)) + .toList(); + } + + throw Exception('获取转让记录失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[TransferService] 获取转让记录失败: $e'); + rethrow; + } + } + + /// 获取转让详情(含状态日志) + Future getTransferDetail(String transferOrderNo) async { + try { + debugPrint('[TransferService] 获取转让详情: $transferOrderNo'); + final response = await _apiClient.get('/transfers/$transferOrderNo'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return TransferOrderDetail.fromJson(data); + } + + throw Exception('获取转让详情失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[TransferService] 获取转让详情失败: $e'); + rethrow; + } + } +} diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 9348cd86..166d1778 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1244,6 +1244,16 @@ class _ProfilePageState extends ConsumerState { context.push(RoutePaths.prePlantingPosition); } + /// [2026-02-19] 进入转让发起页 + void _goToTransferInitiate() { + context.push(RoutePaths.transferInitiate); + } + + /// [2026-02-19] 进入转让记录页 + void _goToTransferList() { + context.push(RoutePaths.transferList); + } + /// 谷歌验证器 void _goToGoogleAuth() { context.push(RoutePaths.googleAuth); @@ -1431,6 +1441,9 @@ class _ProfilePageState extends ConsumerState { const SizedBox(height: 8), // [2026-02-17] 预种计划按钮(购买 + 查看持仓) _buildPrePlantingButtons(), + const SizedBox(height: 8), + // [2026-02-19] 纯新增:树转让按钮(发起转让 + 转让记录) + _buildTransferButtons(), const SizedBox(height: 16), // 主要内容卡片(懒加载:收益数据) VisibilityDetector( @@ -2002,6 +2015,91 @@ class _ProfilePageState extends ConsumerState { ); } + /// [2026-02-19] 构建树转让按钮行(发起转让 + 转让记录) + /// + /// 复用预种按钮的样式,使用绿色系配色区分 + Widget _buildTransferButtons() { + return Row( + children: [ + // 发起转让按钮 + Expanded( + child: GestureDetector( + onTap: _goToTransferInitiate, + child: Container( + height: 44, + decoration: BoxDecoration( + color: const Color(0xFF2E7D32).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFF2E7D32).withValues(alpha: 0.4), + width: 1, + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.swap_horiz, + color: Color(0xFF4CAF50), + size: 18, + ), + SizedBox(width: 6), + Text( + '发起转让', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF4CAF50), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 8), + // 转让记录按钮 + Expanded( + child: GestureDetector( + onTap: _goToTransferList, + child: Container( + height: 44, + decoration: BoxDecoration( + color: const Color(0xFF1565C0).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFF1565C0).withValues(alpha: 0.25), + width: 1, + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.receipt_long_outlined, + color: Color(0xFF42A5F5), + size: 18, + ), + SizedBox(width: 6), + Text( + '转让记录', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF42A5F5), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + /// 构建主要内容卡片 Widget _buildMainContentCard() { // Widget 结构始终保持,数据值根据状态显示 "0" 或实际值 diff --git a/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_detail_page.dart b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_detail_page.dart new file mode 100644 index 00000000..07e81750 --- /dev/null +++ b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_detail_page.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/transfer_service.dart'; + +// ============================================ +// [2026-02-19] 树转让详情页(纯新增) +// ============================================ +// +// 展示单笔转让订单的详细信息: +// - 状态 badge + 基本信息 +// - Saga 进度时间线 +// - 操作按钮(卖方确认 / 取消) +// +// === 回滚方式 === +// 删除 features/transfer/ 目录 + +/// 转让详情页 +class TransferDetailPage extends ConsumerStatefulWidget { + final String transferOrderNo; + + const TransferDetailPage({super.key, required this.transferOrderNo}); + + @override + ConsumerState createState() => _TransferDetailPageState(); +} + +class _TransferDetailPageState extends ConsumerState { + TransferOrderDetail? _order; + bool _isLoading = true; + String? _errorMessage; + bool _isActionLoading = false; + + // Saga 步骤定义 + static const _sagaSteps = [ + {'status': 'PENDING', 'label': '待卖方确认'}, + {'status': 'SELLER_CONFIRMED', 'label': '卖方已确认'}, + {'status': 'PAYMENT_FROZEN', 'label': '买方资金冻结'}, + {'status': 'TREES_LOCKED', 'label': '卖方树锁定'}, + {'status': 'OWNERSHIP_TRANSFERRED', 'label': '所有权变更'}, + {'status': 'CONTRIBUTION_ADJUSTED', 'label': '算力调整'}, + {'status': 'STATS_UPDATED', 'label': '团队统计更新'}, + {'status': 'PAYMENT_SETTLED', 'label': '资金结算'}, + {'status': 'COMPLETED', 'label': '转让完成'}, + ]; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final service = ref.read(transferServiceProvider); + final order = await service.getTransferDetail(widget.transferOrderNo); + if (mounted) { + setState(() { + _order = order; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = e.toString(); + _isLoading = false; + }); + } + } + } + + Future _handleSellerConfirm() async { + setState(() => _isActionLoading = true); + try { + final service = ref.read(transferServiceProvider); + await service.sellerConfirm(widget.transferOrderNo); + if (mounted) _loadData(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('确认失败: $e'), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _isActionLoading = false); + } + } + + Future _handleCancel() async { + final reason = await showDialog( + context: context, + builder: (ctx) { + String text = ''; + return AlertDialog( + title: const Text('取消转让'), + content: TextField( + onChanged: (v) => text = v, + decoration: const InputDecoration(hintText: '取消原因(可选)'), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('返回')), + TextButton( + onPressed: () => Navigator.pop(ctx, text.isEmpty ? '用户主动取消' : text), + child: const Text('确认取消', style: TextStyle(color: Colors.red)), + ), + ], + ); + }, + ); + + if (reason == null) return; + + setState(() => _isActionLoading = true); + try { + final service = ref.read(transferServiceProvider); + await service.cancelTransfer(widget.transferOrderNo, reason: reason); + if (mounted) _loadData(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('取消失败: $e'), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _isActionLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF1A1A2E), + appBar: AppBar( + backgroundColor: const Color(0xFF1A1A2E), + foregroundColor: Colors.white, + title: const Text( + '转让详情', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), + ), + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return const Center(child: CircularProgressIndicator(color: Color(0xFFD4AF37))); + } + if (_errorMessage != null || _order == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('加载失败', style: TextStyle(color: Colors.white.withValues(alpha: 0.7))), + TextButton(onPressed: _loadData, child: const Text('重试', style: TextStyle(color: Color(0xFFD4AF37)))), + ], + ), + ); + } + + final order = _order!; + return RefreshIndicator( + onRefresh: _loadData, + color: const Color(0xFFD4AF37), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoCard(order), + const SizedBox(height: 16), + _buildSagaTimeline(order), + const SizedBox(height: 16), + if (order.statusLogs.isNotEmpty) _buildStatusLogs(order), + const SizedBox(height: 16), + _buildActionButtons(order), + ], + ), + ), + ); + } + + Widget _buildInfoCard(TransferOrderDetail order) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 单号 + 状态 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + order.transferOrderNo, + style: const TextStyle(fontSize: 14, color: Colors.white70, fontFamily: 'Inter'), + overflow: TextOverflow.ellipsis, + ), + ), + _buildStatusBadge(order.status), + ], + ), + const SizedBox(height: 16), + _infoRow('卖方', order.sellerAccountSequence), + _infoRow('买方', order.buyerAccountSequence), + _infoRow('棵数', '${order.treeCount} 棵'), + _infoRow('单价', '${order.pricePerTree.toStringAsFixed(0)} USDT'), + _infoRow('总价', '${order.totalPrice.toStringAsFixed(0)} USDT'), + _infoRow('手续费', '${order.platformFeeAmount.toStringAsFixed(2)} USDT (${(order.platformFeeRate * 100).toStringAsFixed(1)}%)'), + _infoRow('卖方到账', '${order.sellerReceiveAmount.toStringAsFixed(2)} USDT'), + _infoRow('创建时间', _formatDateTime(order.createdAt)), + if (order.completedAt != null) + _infoRow('完成时间', _formatDateTime(order.completedAt!)), + if (order.cancelReason != null) + _infoRow('取消原因', order.cancelReason!), + ], + ), + ); + } + + Widget _infoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.5))), + Text(value, style: const TextStyle(fontSize: 13, color: Colors.white, fontFamily: 'Inter')), + ], + ), + ); + } + + Widget _buildSagaTimeline(TransferOrderDetail order) { + final currentStatusStr = order.status.name.toUpperCase(); + // Map enum name to API status string + final statusApiMap = { + 'pending': 'PENDING', + 'sellerConfirmed': 'SELLER_CONFIRMED', + 'paymentFrozen': 'PAYMENT_FROZEN', + 'treesLocked': 'TREES_LOCKED', + 'ownershipTransferred': 'OWNERSHIP_TRANSFERRED', + 'contributionAdjusted': 'CONTRIBUTION_ADJUSTED', + 'statsUpdated': 'STATS_UPDATED', + 'paymentSettled': 'PAYMENT_SETTLED', + 'completed': 'COMPLETED', + }; + final apiStatus = statusApiMap[order.status.name] ?? currentStatusStr; + final currentIdx = _sagaSteps.indexWhere((s) => s['status'] == apiStatus); + final isFailed = order.status == TransferOrderStatus.failed || + order.status == TransferOrderStatus.rollingBack || + order.status == TransferOrderStatus.rolledBack; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Saga 进度', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + ), + const SizedBox(height: 16), + ...List.generate(_sagaSteps.length, (idx) { + final step = _sagaSteps[idx]; + final isCompleted = !isFailed && (idx < currentIdx || (order.isTerminal && idx <= currentIdx)); + final isCurrent = idx == currentIdx && !order.isTerminal && !isFailed; + final isFailedStep = isFailed && idx == currentIdx; + + Color dotColor = Colors.white.withValues(alpha: 0.2); + Color dotBorder = Colors.white.withValues(alpha: 0.2); + if (isCompleted) { + dotColor = const Color(0xFF52C41A); + dotBorder = const Color(0xFF52C41A); + } else if (isCurrent) { + dotColor = const Color(0xFFD4AF37).withValues(alpha: 0.3); + dotBorder = const Color(0xFFD4AF37); + } else if (isFailedStep) { + dotColor = const Color(0xFFFF4D4F); + dotBorder = const Color(0xFFFF4D4F); + } + + // Find log entry for this step + final logEntry = order.statusLogs + .where((l) => l.toStatus == step['status']) + .toList(); + final logTime = logEntry.isNotEmpty ? logEntry.first.createdAt : null; + + return Padding( + padding: EdgeInsets.only(bottom: idx < _sagaSteps.length - 1 ? 16 : 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: dotColor, + shape: BoxShape.circle, + border: Border.all(color: dotBorder, width: 2), + ), + child: isCompleted + ? const Icon(Icons.check, size: 12, color: Colors.white) + : isFailedStep + ? const Icon(Icons.close, size: 12, color: Colors.white) + : null, + ), + if (idx < _sagaSteps.length - 1) + Container( + width: 2, + height: 20, + color: isCompleted + ? const Color(0xFF52C41A).withValues(alpha: 0.4) + : Colors.white.withValues(alpha: 0.1), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + step['label']!, + style: TextStyle( + fontSize: 14, + fontWeight: (isCurrent || isCompleted) ? FontWeight.w600 : FontWeight.w400, + color: (isCurrent || isCompleted) + ? Colors.white + : Colors.white.withValues(alpha: 0.4), + ), + ), + if (logTime != null) + Text( + _formatDateTime(logTime), + style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: 0.3)), + ), + ], + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + Widget _buildStatusLogs(TransferOrderDetail order) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '状态变更日志', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + ), + const SizedBox(height: 12), + ...order.statusLogs.map((log) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text( + '${log.fromStatus} → ${log.toStatus}', + style: const TextStyle(fontSize: 12, color: Colors.white70, fontFamily: 'Inter'), + ), + ), + Expanded( + child: Text( + _formatDateTime(log.createdAt), + style: TextStyle(fontSize: 11, color: Colors.white.withValues(alpha: 0.4)), + textAlign: TextAlign.right, + ), + ), + ], + ), + )), + ], + ), + ); + } + + Widget _buildActionButtons(TransferOrderDetail order) { + if (order.isTerminal) return const SizedBox.shrink(); + + return Row( + children: [ + // 卖方确认按钮 + if (order.isPendingSellerConfirm) + Expanded( + child: ElevatedButton( + onPressed: _isActionLoading ? null : _handleSellerConfirm, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD4AF37), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text(_isActionLoading ? '处理中...' : '确认转让'), + ), + ), + if (order.isPendingSellerConfirm) const SizedBox(width: 12), + // 取消按钮 + if (order.isCancellable) + Expanded( + child: OutlinedButton( + onPressed: _isActionLoading ? null : _handleCancel, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('取消转让'), + ), + ), + ], + ); + } + + Widget _buildStatusBadge(TransferOrderStatus status) { + final info = _getStatusInfo(status); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: info.color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + info.label, + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: info.color), + ), + ); + } + + _StatusInfo _getStatusInfo(TransferOrderStatus status) { + switch (status) { + case TransferOrderStatus.pending: + return _StatusInfo('待确认', const Color(0xFFD48806)); + case TransferOrderStatus.sellerConfirmed: + return _StatusInfo('卖方已确认', const Color(0xFF096DD9)); + case TransferOrderStatus.paymentFrozen: + case TransferOrderStatus.treesLocked: + case TransferOrderStatus.ownershipTransferred: + case TransferOrderStatus.contributionAdjusted: + case TransferOrderStatus.statsUpdated: + case TransferOrderStatus.paymentSettled: + return _StatusInfo('处理中', const Color(0xFF2F54EB)); + case TransferOrderStatus.completed: + return _StatusInfo('已完成', const Color(0xFF52C41A)); + case TransferOrderStatus.cancelled: + return _StatusInfo('已取消', const Color(0xFF8C8C8C)); + case TransferOrderStatus.failed: + return _StatusInfo('失败', const Color(0xFFCF1322)); + case TransferOrderStatus.rollingBack: + return _StatusInfo('补偿中', const Color(0xFFD4380D)); + case TransferOrderStatus.rolledBack: + return _StatusInfo('已回滚', const Color(0xFF595959)); + } + } + + String _formatDateTime(DateTime dt) { + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } +} + +class _StatusInfo { + final String label; + final Color color; + _StatusInfo(this.label, this.color); +} diff --git a/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_initiate_page.dart b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_initiate_page.dart new file mode 100644 index 00000000..0a59c17c --- /dev/null +++ b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_initiate_page.dart @@ -0,0 +1,335 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../routes/route_paths.dart'; + +// ============================================ +// [2026-02-19] 树转让发起页(纯新增) +// ============================================ +// +// 用户发起树转让的表单页面: +// - 输入买方账号 +// - 输入每棵售价 +// - 自动计算手续费(5%)和卖方到账金额 +// - 确认发起转让 +// +// === 回滚方式 === +// 删除 features/transfer/ 目录 + +/// 转让发起页 +class TransferInitiatePage extends ConsumerStatefulWidget { + const TransferInitiatePage({super.key}); + + @override + ConsumerState createState() => _TransferInitiatePageState(); +} + +class _TransferInitiatePageState extends ConsumerState { + final _buyerController = TextEditingController(); + final _priceController = TextEditingController(); + final _treeCountController = TextEditingController(text: '1'); + bool _isSubmitting = false; + String? _errorMessage; + + // 平台手续费率 + static const double _feeRate = 0.05; + + @override + void dispose() { + _buyerController.dispose(); + _priceController.dispose(); + _treeCountController.dispose(); + super.dispose(); + } + + double get _pricePerTree => double.tryParse(_priceController.text) ?? 0; + int get _treeCount => int.tryParse(_treeCountController.text) ?? 1; + double get _totalPrice => _pricePerTree * _treeCount; + double get _feeAmount => _totalPrice * _feeRate; + double get _sellerReceive => _totalPrice - _feeAmount; + + Future _handleSubmit() async { + final buyer = _buyerController.text.trim(); + if (buyer.isEmpty) { + setState(() => _errorMessage = '请输入买方账号'); + return; + } + if (_pricePerTree <= 0) { + setState(() => _errorMessage = '请输入有效的每棵售价'); + return; + } + if (_treeCount <= 0) { + setState(() => _errorMessage = '请输入有效的转让棵数'); + return; + } + + // 确认对话框 + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('确认发起转让'), + content: Text( + '买方账号: $buyer\n' + '转让棵数: $_treeCount 棵\n' + '每棵售价: ${_pricePerTree.toStringAsFixed(0)} USDT\n' + '总价: ${_totalPrice.toStringAsFixed(0)} USDT\n' + '手续费: ${_feeAmount.toStringAsFixed(2)} USDT (${(_feeRate * 100).toStringAsFixed(0)}%)\n' + '您将收到: ${_sellerReceive.toStringAsFixed(2)} USDT\n\n' + '转让发起后需要卖方确认,确认后 Saga 流程将自动执行。', + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFD4AF37)), + child: const Text('确认发起'), + ), + ], + ), + ); + + if (confirmed != true) return; + + setState(() { + _isSubmitting = true; + _errorMessage = null; + }); + + try { + final service = ref.read(transferServiceProvider); + final order = await service.createTransfer( + buyerAccountSequence: buyer, + plantingOrderNos: [], // 由后端根据棵数自动选择 + pricePerTree: _pricePerTree, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('转让发起成功!等待卖方确认'), + backgroundColor: Color(0xFF52C41A), + ), + ); + // 跳转到详情页 + context.pushReplacement('${RoutePaths.transferDetail}/${order.transferOrderNo}'); + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = e.toString(); + _isSubmitting = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF1A1A2E), + appBar: AppBar( + backgroundColor: const Color(0xFF1A1A2E), + foregroundColor: Colors.white, + title: const Text( + '发起转让', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 说明卡片 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFD4AF37).withValues(alpha: 0.3)), + ), + child: const Row( + children: [ + Icon(Icons.info_outline, color: Color(0xFFD4AF37), size: 20), + SizedBox(width: 8), + Expanded( + child: Text( + '转让已认种的果树所有权。发起后需卖方确认,系统将自动完成资金冻结、所有权变更、算力调整等流程。', + style: TextStyle(fontSize: 13, color: Color(0xFFD4AF37)), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // 买方账号 + _buildLabel('买方账号'), + const SizedBox(height: 8), + _buildTextField( + controller: _buyerController, + hintText: '输入买方账号(如 D25121400002)', + keyboardType: TextInputType.text, + ), + const SizedBox(height: 20), + + // 转让棵数 + _buildLabel('转让棵数'), + const SizedBox(height: 8), + _buildTextField( + controller: _treeCountController, + hintText: '输入棵数', + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 20), + + // 每棵售价 + _buildLabel('每棵售价 (USDT)'), + const SizedBox(height: 8), + _buildTextField( + controller: _priceController, + hintText: '输入每棵售价', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.]'))], + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + + // 费用计算卡片 + if (_pricePerTree > 0) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Column( + children: [ + _feeRow('总价', '${_totalPrice.toStringAsFixed(0)} USDT'), + _feeRow('手续费 (${(_feeRate * 100).toStringAsFixed(0)}%)', '${_feeAmount.toStringAsFixed(2)} USDT'), + const Divider(color: Colors.white24), + _feeRow('您将收到', '${_sellerReceive.toStringAsFixed(2)} USDT', + valueColor: const Color(0xFFD4AF37)), + ], + ), + ), + const SizedBox(height: 24), + ], + + // 错误信息 + if (_errorMessage != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text(_errorMessage!, style: const TextStyle(color: Colors.red, fontSize: 13)), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + + // 提交按钮 + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD4AF37), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + disabledBackgroundColor: const Color(0xFFD4AF37).withValues(alpha: 0.4), + ), + child: Text( + _isSubmitting ? '提交中...' : '发起转让', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildLabel(String text) { + return Text( + text, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String hintText, + TextInputType? keyboardType, + List? inputFormatters, + ValueChanged? onChanged, + }) { + return TextField( + controller: controller, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + onChanged: onChanged, + style: const TextStyle(color: Colors.white, fontSize: 15, fontFamily: 'Inter'), + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.3)), + filled: true, + fillColor: const Color(0xFF16213E), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFD4AF37)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ); + } + + Widget _feeRow(String label, String value, {Color? valueColor}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: 0.5))), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: valueColor ?? Colors.white, + fontFamily: 'Inter', + ), + ), + ], + ), + ); + } +} diff --git a/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_list_page.dart b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_list_page.dart new file mode 100644 index 00000000..b621e603 --- /dev/null +++ b/frontend/mobile-app/lib/features/transfer/presentation/pages/transfer_list_page.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/transfer_service.dart'; +import '../../../../routes/route_paths.dart'; + +// ============================================ +// [2026-02-19] 树转让记录列表页(纯新增) +// ============================================ +// +// 展示用户的树转让记录,包括作为卖方和买方的所有转让订单。 +// +// === 页面结构 === +// ┌─ AppBar(转让记录) +// ├─ Tab: 全部 / 转出 / 转入 +// └─ ListView(转让订单卡片列表) +// +// === 回滚方式 === +// 删除 features/transfer/ 目录 + +/// 转让记录列表页 +class TransferListPage extends ConsumerStatefulWidget { + const TransferListPage({super.key}); + + @override + ConsumerState createState() => _TransferListPageState(); +} + +class _TransferListPageState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + List _orders = []; + bool _isLoading = true; + String? _errorMessage; + + // Tab 角色过滤: null=全部, 'seller'=转出, 'buyer'=转入 + final List _tabRoles = [null, 'seller', 'buyer']; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(() { + if (!_tabController.indexIsChanging) { + _loadData(); + } + }); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final service = ref.read(transferServiceProvider); + final role = _tabRoles[_tabController.index]; + final orders = await service.getMyTransfers(role: role); + if (mounted) { + setState(() { + _orders = orders; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = e.toString(); + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF1A1A2E), + appBar: AppBar( + backgroundColor: const Color(0xFF1A1A2E), + foregroundColor: Colors.white, + title: const Text( + '转让记录', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + bottom: TabBar( + controller: _tabController, + indicatorColor: const Color(0xFFD4AF37), + labelColor: const Color(0xFFD4AF37), + unselectedLabelColor: Colors.white54, + tabs: const [ + Tab(text: '全部'), + Tab(text: '转出'), + Tab(text: '转入'), + ], + ), + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return const Center( + child: CircularProgressIndicator(color: Color(0xFFD4AF37)), + ); + } + + if (_errorMessage != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '加载失败', + style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 16), + ), + const SizedBox(height: 8), + TextButton( + onPressed: _loadData, + child: const Text('重试', style: TextStyle(color: Color(0xFFD4AF37))), + ), + ], + ), + ); + } + + if (_orders.isEmpty) { + return Center( + child: Text( + '暂无转让记录', + style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 16), + ), + ); + } + + return RefreshIndicator( + onRefresh: _loadData, + color: const Color(0xFFD4AF37), + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _orders.length, + itemBuilder: (context, index) => _buildOrderCard(_orders[index]), + ), + ); + } + + Widget _buildOrderCard(TransferOrder order) { + final statusInfo = _getStatusInfo(order.status); + + return GestureDetector( + onTap: () { + context.push('${RoutePaths.transferDetail}/${order.transferOrderNo}'); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withValues(alpha: 0.08), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部:单号 + 状态 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + order.transferOrderNo, + style: const TextStyle( + fontSize: 13, + fontFamily: 'Inter', + color: Colors.white70, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: statusInfo.color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + statusInfo.label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: statusInfo.color, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + // 卖方 → 买方 + Row( + children: [ + Text( + order.sellerAccountSequence, + style: const TextStyle(fontSize: 14, color: Colors.white, fontFamily: 'Inter'), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Icon(Icons.arrow_forward, size: 16, color: Color(0xFFD4AF37)), + ), + Text( + order.buyerAccountSequence, + style: const TextStyle(fontSize: 14, color: Colors.white, fontFamily: 'Inter'), + ), + ], + ), + const SizedBox(height: 8), + // 棵数 + 价格 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${order.treeCount} 棵', + style: const TextStyle(fontSize: 14, color: Colors.white70), + ), + Text( + '${order.totalPrice.toStringAsFixed(0)} USDT', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + fontFamily: 'Inter', + ), + ), + ], + ), + const SizedBox(height: 6), + // 时间 + Text( + _formatDateTime(order.createdAt), + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.4)), + ), + ], + ), + ), + ); + } + + _StatusInfo _getStatusInfo(TransferOrderStatus status) { + switch (status) { + case TransferOrderStatus.pending: + return _StatusInfo('待确认', const Color(0xFFD48806)); + case TransferOrderStatus.sellerConfirmed: + return _StatusInfo('卖方已确认', const Color(0xFF096DD9)); + case TransferOrderStatus.paymentFrozen: + case TransferOrderStatus.treesLocked: + case TransferOrderStatus.ownershipTransferred: + case TransferOrderStatus.contributionAdjusted: + case TransferOrderStatus.statsUpdated: + case TransferOrderStatus.paymentSettled: + return _StatusInfo('处理中', const Color(0xFF2F54EB)); + case TransferOrderStatus.completed: + return _StatusInfo('已完成', const Color(0xFF52C41A)); + case TransferOrderStatus.cancelled: + return _StatusInfo('已取消', const Color(0xFF8C8C8C)); + case TransferOrderStatus.failed: + return _StatusInfo('失败', const Color(0xFFCF1322)); + case TransferOrderStatus.rollingBack: + return _StatusInfo('补偿中', const Color(0xFFD4380D)); + case TransferOrderStatus.rolledBack: + return _StatusInfo('已回滚', const Color(0xFF595959)); + } + } + + String _formatDateTime(DateTime dt) { + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } +} + +class _StatusInfo { + final String label; + final Color color; + _StatusInfo(this.label, this.color); +} diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index d3db4281..6d6c432f 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -48,6 +48,10 @@ import '../features/pending_actions/presentation/pages/pending_actions_page.dart import '../features/pre_planting/presentation/pages/pre_planting_purchase_page.dart'; import '../features/pre_planting/presentation/pages/pre_planting_position_page.dart'; import '../features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart'; +// [2026-02-19] 纯新增:树转让页面 +import '../features/transfer/presentation/pages/transfer_list_page.dart'; +import '../features/transfer/presentation/pages/transfer_detail_page.dart'; +import '../features/transfer/presentation/pages/transfer_initiate_page.dart'; import 'route_paths.dart'; import 'route_names.dart'; @@ -462,6 +466,30 @@ final appRouterProvider = Provider((ref) { }, ), + // [2026-02-19] Transfer List Page (树转让 - 记录列表) + GoRoute( + path: RoutePaths.transferList, + name: RouteNames.transferList, + builder: (context, state) => const TransferListPage(), + ), + + // [2026-02-19] Transfer Detail Page (树转让 - 详情) + GoRoute( + path: '${RoutePaths.transferDetail}/:transferOrderNo', + name: RouteNames.transferDetail, + builder: (context, state) { + final transferOrderNo = state.pathParameters['transferOrderNo'] ?? ''; + return TransferDetailPage(transferOrderNo: transferOrderNo); + }, + ), + + // [2026-02-19] Transfer Initiate Page (树转让 - 发起) + GoRoute( + path: RoutePaths.transferInitiate, + name: RouteNames.transferInitiate, + builder: (context, state) => const TransferInitiatePage(), + ), + // Pending Contracts Page (待签署合同列表) // 注意:必须放在 contractSigning/:orderNo 前面,否则 "pending" 会被当成 orderNo GoRoute( diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index ad988dd6..a8db9ef0 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -60,6 +60,11 @@ class RouteNames { static const prePlantingPosition = 'pre-planting-position'; // 持仓页 static const prePlantingMergeDetail = 'pre-planting-merge'; // 合并详情页 + // [2026-02-19] Transfer (树转让) + static const transferList = 'transfer-list'; // 转让记录列表 + static const transferDetail = 'transfer-detail'; // 转让详情 + static const transferInitiate = 'transfer-initiate'; // 发起转让 + // Contract Signing (合同签署) static const contractSigning = 'contract-signing'; static const pendingContracts = 'pending-contracts'; diff --git a/frontend/mobile-app/lib/routes/route_paths.dart b/frontend/mobile-app/lib/routes/route_paths.dart index 729fc5cf..dd41ac04 100644 --- a/frontend/mobile-app/lib/routes/route_paths.dart +++ b/frontend/mobile-app/lib/routes/route_paths.dart @@ -60,6 +60,11 @@ class RoutePaths { static const prePlantingPosition = '/pre-planting/position'; // 持仓页 static const prePlantingMergeDetail = '/pre-planting/merge'; // 合并详情页 + // [2026-02-19] Transfer (树转让) + static const transferList = '/transfer/list'; // 转让记录列表 + static const transferDetail = '/transfer/detail'; // 转让详情 + static const transferInitiate = '/transfer/initiate'; // 发起转让 + // Contract Signing (合同签署) static const contractSigning = '/contract-signing'; static const pendingContracts = '/contract-signing/pending';