rwadurian/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts

212 lines
7.0 KiB
TypeScript

import { RewardLedgerEntry } from './reward-ledger-entry.aggregate';
import { RewardSource } from '../../value-objects/reward-source.vo';
import { RightType } from '../../value-objects/right-type.enum';
import { RewardStatus } from '../../value-objects/reward-status.enum';
import { Money } from '../../value-objects/money.vo';
import { Hashpower } from '../../value-objects/hashpower.vo';
describe('RewardLedgerEntry', () => {
const createRewardSource = () =>
RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(2));
describe('createPending', () => {
it('should create a pending reward with 24h expiration', () => {
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
memo: 'Test reward',
});
expect(entry.isPending).toBe(true);
expect(entry.rewardStatus).toBe(RewardStatus.PENDING);
expect(entry.usdtAmount.amount).toBe(500);
expect(entry.expireAt).toBeDefined();
expect(entry.domainEvents.length).toBe(1);
expect(entry.domainEvents[0].eventType).toBe('RewardCreated');
});
it('should set expireAt to 24 hours from now', () => {
const before = Date.now();
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
const after = Date.now();
const expectedExpireMin = before + 24 * 60 * 60 * 1000;
const expectedExpireMax = after + 24 * 60 * 60 * 1000;
expect(entry.expireAt!.getTime()).toBeGreaterThanOrEqual(expectedExpireMin);
expect(entry.expireAt!.getTime()).toBeLessThanOrEqual(expectedExpireMax);
});
});
describe('createSettleable', () => {
it('should create a settleable reward without expiration', () => {
const entry = RewardLedgerEntry.createSettleable({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.create(5),
memo: 'Direct reward',
});
expect(entry.isSettleable).toBe(true);
expect(entry.rewardStatus).toBe(RewardStatus.SETTLEABLE);
expect(entry.expireAt).toBeNull();
expect(entry.hashpowerAmount.value).toBe(5);
});
});
describe('claim', () => {
it('should transition pending to settleable', () => {
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
entry.clearDomainEvents();
entry.claim();
expect(entry.isSettleable).toBe(true);
expect(entry.claimedAt).toBeDefined();
expect(entry.expireAt).toBeNull();
expect(entry.domainEvents.length).toBe(1);
expect(entry.domainEvents[0].eventType).toBe('RewardClaimed');
});
it('should throw error when not pending', () => {
const entry = RewardLedgerEntry.createSettleable({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
expect(() => entry.claim()).toThrow('只有待领取状态才能领取');
});
});
describe('expire', () => {
it('should transition pending to expired', () => {
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
entry.clearDomainEvents();
entry.expire();
expect(entry.isExpired).toBe(true);
expect(entry.expiredAt).toBeDefined();
expect(entry.domainEvents.length).toBe(1);
expect(entry.domainEvents[0].eventType).toBe('RewardExpired');
});
it('should throw error when not pending', () => {
const entry = RewardLedgerEntry.createSettleable({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
expect(() => entry.expire()).toThrow('只有待领取状态才能过期');
});
});
describe('settle', () => {
it('should transition settleable to settled', () => {
const entry = RewardLedgerEntry.createSettleable({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
entry.clearDomainEvents();
entry.settle('BNB', 0.25);
expect(entry.isSettled).toBe(true);
expect(entry.settledAt).toBeDefined();
expect(entry.domainEvents.length).toBe(1);
expect(entry.domainEvents[0].eventType).toBe('RewardSettled');
});
it('should throw error when not settleable', () => {
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
expect(() => entry.settle('BNB', 0.25)).toThrow('只有可结算状态才能结算');
});
});
describe('getRemainingTimeMs', () => {
it('should return remaining time for pending rewards', () => {
const entry = RewardLedgerEntry.createPending({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
const remaining = entry.getRemainingTimeMs();
const expected24h = 24 * 60 * 60 * 1000;
expect(remaining).toBeGreaterThan(expected24h - 1000); // Allow 1 second tolerance
expect(remaining).toBeLessThanOrEqual(expected24h);
});
it('should return 0 for settleable rewards', () => {
const entry = RewardLedgerEntry.createSettleable({
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: Money.USDT(500),
hashpowerAmount: Hashpower.zero(),
});
expect(entry.getRemainingTimeMs()).toBe(0);
});
});
describe('reconstitute', () => {
it('should rebuild aggregate from persistence data', () => {
const data = {
id: BigInt(1),
userId: BigInt(100),
rewardSource: createRewardSource(),
usdtAmount: 500,
hashpowerAmount: 5,
rewardStatus: RewardStatus.SETTLEABLE,
createdAt: new Date(),
expireAt: null,
claimedAt: new Date(),
settledAt: null,
expiredAt: null,
memo: 'Test',
};
const entry = RewardLedgerEntry.reconstitute(data);
expect(entry.id).toBe(BigInt(1));
expect(entry.userId).toBe(BigInt(100));
expect(entry.usdtAmount.amount).toBe(500);
expect(entry.hashpowerAmount.value).toBe(5);
expect(entry.isSettleable).toBe(true);
expect(entry.domainEvents.length).toBe(0); // No events on reconstitute
});
});
});