rwadurian/backend/services/reward-service/test/integration/reward-application.service....

362 lines
13 KiB
TypeScript

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>(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);
});
});
});