import { Test, TestingModule } from '@nestjs/testing'; import { RewardApplicationService } from '../../src/application/services/reward-application.service'; import { RewardCalculationService } from '../../src/domain/services/reward-calculation.service'; import { RewardExpirationService } from '../../src/domain/services/reward-expiration.service'; import { REWARD_LEDGER_ENTRY_REPOSITORY } from '../../src/domain/repositories/reward-ledger-entry.repository.interface'; import { REWARD_SUMMARY_REPOSITORY } from '../../src/domain/repositories/reward-summary.repository.interface'; import { EventPublisherService } from '../../src/infrastructure/kafka/event-publisher.service'; import { WalletServiceClient } from '../../src/infrastructure/external/wallet-service/wallet-service.client'; import { RewardLedgerEntry } from '../../src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; import { RewardSummary } from '../../src/domain/aggregates/reward-summary/reward-summary.aggregate'; import { RewardSource } from '../../src/domain/value-objects/reward-source.vo'; import { RightType } from '../../src/domain/value-objects/right-type.enum'; import { RewardStatus } from '../../src/domain/value-objects/reward-status.enum'; import { Money } from '../../src/domain/value-objects/money.vo'; import { Hashpower } from '../../src/domain/value-objects/hashpower.vo'; describe('RewardApplicationService (Integration)', () => { let service: RewardApplicationService; let mockLedgerRepository: any; let mockSummaryRepository: any; let mockEventPublisher: any; let mockWalletService: any; let mockCalculationService: any; beforeEach(async () => { mockLedgerRepository = { save: jest.fn(), saveAll: jest.fn(), findPendingByUserId: jest.fn(), findSettleableByUserId: jest.fn(), findExpiredPending: jest.fn(), findByUserId: jest.fn(), countByUserId: jest.fn(), }; mockSummaryRepository = { getOrCreate: jest.fn(), findByUserId: jest.fn(), save: jest.fn(), }; mockEventPublisher = { publishAll: jest.fn(), }; mockWalletService = { executeSwap: jest.fn(), }; mockCalculationService = { calculateRewards: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ RewardApplicationService, { provide: RewardCalculationService, useValue: mockCalculationService, }, { provide: RewardExpirationService, useValue: { expireOverdueRewards: jest.fn((r) => r) }, }, { provide: REWARD_LEDGER_ENTRY_REPOSITORY, useValue: mockLedgerRepository, }, { provide: REWARD_SUMMARY_REPOSITORY, useValue: mockSummaryRepository, }, { provide: EventPublisherService, useValue: mockEventPublisher, }, { provide: WalletServiceClient, useValue: mockWalletService, }, ], }).compile(); service = module.get(RewardApplicationService); }); describe('distributeRewards', () => { it('should distribute rewards and update summaries', async () => { const params = { sourceOrderNo: 'ORDER001', sourceUserId: BigInt(100), treeCount: 10, provinceCode: '440000', cityCode: '440100', }; const mockReward = RewardLedgerEntry.createPending({ userId: BigInt(200), accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(100)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test', }); const mockSummary = RewardSummary.create(BigInt(200), BigInt(100)); mockCalculationService.calculateRewards.mockResolvedValue([mockReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); await service.distributeRewards(params); expect(mockCalculationService.calculateRewards).toHaveBeenCalledWith(params); expect(mockLedgerRepository.saveAll).toHaveBeenCalledWith([mockReward]); expect(mockSummaryRepository.save).toHaveBeenCalled(); expect(mockEventPublisher.publishAll).toHaveBeenCalled(); }); }); describe('claimPendingRewardsForUser', () => { it('should claim pending rewards and move to settleable', async () => { const userId = BigInt(100); const pendingReward = RewardLedgerEntry.createPending({ userId, accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test reward', }); const mockSummary = RewardSummary.create(userId, BigInt(100)); mockSummary.addPending( Money.USDT(500), Hashpower.zero(), new Date(Date.now() + 24 * 60 * 60 * 1000), ); mockLedgerRepository.findPendingByUserId.mockResolvedValue([pendingReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); const result = await service.claimPendingRewardsForUser(userId); expect(result.claimedCount).toBe(1); expect(result.totalUsdtClaimed).toBe(500); expect(mockLedgerRepository.save).toHaveBeenCalled(); expect(mockSummaryRepository.save).toHaveBeenCalled(); }); it('should skip expired rewards', async () => { const userId = BigInt(100); const expiredReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId, accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.PENDING, createdAt: new Date(Date.now() - 25 * 60 * 60 * 1000), expireAt: new Date(Date.now() - 1000), claimedAt: null, settledAt: null, expiredAt: null, memo: 'Expired reward', }); const mockSummary = RewardSummary.create(userId, BigInt(100)); mockLedgerRepository.findPendingByUserId.mockResolvedValue([expiredReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); const result = await service.claimPendingRewardsForUser(userId); expect(result.claimedCount).toBe(0); expect(result.totalUsdtClaimed).toBe(0); }); }); describe('settleRewards', () => { it('should settle rewards and call wallet service', async () => { const accountSequence = BigInt(100); const settleableReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId: BigInt(100), accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.SETTLEABLE, createdAt: new Date(), expireAt: null, claimedAt: new Date(), settledAt: null, expiredAt: null, memo: 'Test', }); const mockSummary = RewardSummary.create(BigInt(100), BigInt(100)); mockSummary.addSettleable(Money.USDT(500), Hashpower.zero()); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([settleableReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); mockWalletService.executeSwap.mockResolvedValue({ success: true, receivedAmount: 0.25, txHash: '0x123', }); const result = await service.settleRewards({ accountSequence, settleCurrency: 'BNB', }); expect(result.success).toBe(true); expect(result.settledUsdtAmount).toBe(500); expect(result.receivedAmount).toBe(0.25); expect(result.settleCurrency).toBe('BNB'); expect(mockWalletService.executeSwap).toHaveBeenCalledWith({ userId: BigInt(100), usdtAmount: 500, targetCurrency: 'BNB', }); }); it('should return error when no settleable rewards', async () => { const accountSequence = BigInt(100); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([]); const result = await service.settleRewards({ accountSequence, settleCurrency: 'BNB', }); expect(result.success).toBe(false); expect(result.error).toBe('没有可结算的收益'); }); it('should handle wallet service failure', async () => { const accountSequence = BigInt(100); const settleableReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId: BigInt(100), accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.SETTLEABLE, createdAt: new Date(), expireAt: null, claimedAt: new Date(), settledAt: null, expiredAt: null, memo: 'Test', }); const mockSummary = RewardSummary.create(BigInt(100), BigInt(100)); mockSummary.addSettleable(Money.USDT(500), Hashpower.zero()); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([settleableReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); mockWalletService.executeSwap.mockResolvedValue({ success: false, error: 'Insufficient liquidity', }); const result = await service.settleRewards({ accountSequence, settleCurrency: 'BNB', }); expect(result.success).toBe(false); expect(result.error).toBe('Insufficient liquidity'); }); }); describe('getRewardSummary', () => { it('should return reward summary for user', async () => { const userId = BigInt(100); const mockSummary = RewardSummary.create(userId, BigInt(100)); mockSummaryRepository.findByUserId.mockResolvedValue(mockSummary); const result = await service.getRewardSummary(userId); expect(result.pendingUsdt).toBe(0); expect(result.settleableUsdt).toBe(0); expect(result.settledTotalUsdt).toBe(0); expect(result.expiredTotalUsdt).toBe(0); }); it('should return zero values when no summary exists', async () => { const userId = BigInt(100); mockSummaryRepository.findByUserId.mockResolvedValue(null); const result = await service.getRewardSummary(userId); expect(result.pendingUsdt).toBe(0); expect(result.settleableUsdt).toBe(0); }); }); describe('getRewardDetails', () => { it('should return paginated reward details', async () => { const userId = BigInt(100); const mockReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId, accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.PENDING, createdAt: new Date(), expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000), claimedAt: null, settledAt: null, expiredAt: null, memo: 'Test', }); mockLedgerRepository.findByUserId.mockResolvedValue([mockReward]); mockLedgerRepository.countByUserId.mockResolvedValue(1); const result = await service.getRewardDetails(userId, {}, { page: 1, pageSize: 20 }); expect(result.data).toHaveLength(1); expect(result.data[0].usdtAmount).toBe(500); expect(result.pagination.total).toBe(1); }); }); describe('getPendingRewards', () => { it('should return pending rewards with countdown', async () => { const userId = BigInt(100); const pendingReward = RewardLedgerEntry.createPending({ userId, accountSequence: BigInt(100), rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test', }); mockLedgerRepository.findPendingByUserId.mockResolvedValue([pendingReward]); const result = await service.getPendingRewards(userId); expect(result).toHaveLength(1); expect(result[0].usdtAmount).toBe(500); expect(result[0].remainingTimeMs).toBeGreaterThan(0); }); }); });