feat(admin): add Admin module with moderation, user mgmt, and dashboard

- Commands: ApproveListing, RejectListing, BanUser, AdjustSubscription
- Queries: GetModerationQueue, GetDashboardStats, GetRevenueStats
- Admin-only guards via @Roles('ADMIN') on all endpoints
- Prisma-based admin query repository for dashboard aggregations
- 14 unit tests covering all command handlers and query handlers
- Added activate() method to UserEntity for unban support

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 02:17:09 +07:00
parent ac3947b42d
commit dafed32e11
49 changed files with 1485 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import { SubscriptionEntity } from '../entities/subscription.entity';
describe('SubscriptionEntity', () => {
const makeSub = (overrides?: Partial<Parameters<typeof SubscriptionEntity.createNew>[0]>) => {
return SubscriptionEntity.createNew(
'sub-1',
'user-1',
'plan-1',
'AGENT_PRO',
new Date('2026-01-01'),
new Date('2026-02-01'),
);
};
it('creates a new subscription with ACTIVE status', () => {
const sub = makeSub();
expect(sub.id).toBe('sub-1');
expect(sub.userId).toBe('user-1');
expect(sub.planTier).toBe('AGENT_PRO');
expect(sub.status).toBe('ACTIVE');
expect(sub.isActive()).toBe(true);
expect(sub.cancelledAt).toBeNull();
});
it('emits SubscriptionCreatedEvent on creation', () => {
const sub = makeSub();
const events = sub.domainEvents;
expect(events).toHaveLength(1);
expect(events[0].eventName).toBe('subscription.created');
});
it('upgrades plan tier', () => {
const sub = makeSub();
sub.clearDomainEvents();
sub.upgrade('plan-2', 'ENTERPRISE');
expect(sub.planId).toBe('plan-2');
expect(sub.planTier).toBe('ENTERPRISE');
const events = sub.domainEvents;
expect(events).toHaveLength(1);
expect(events[0].eventName).toBe('subscription.upgraded');
});
it('throws when upgrading non-active subscription', () => {
const sub = makeSub();
sub.cancel();
expect(() => sub.upgrade('plan-2', 'ENTERPRISE')).toThrow();
});
it('cancels subscription', () => {
const sub = makeSub();
sub.clearDomainEvents();
sub.cancel();
expect(sub.status).toBe('CANCELLED');
expect(sub.cancelledAt).not.toBeNull();
const events = sub.domainEvents;
expect(events).toHaveLength(1);
expect(events[0].eventName).toBe('subscription.cancelled');
});
it('throws when cancelling already cancelled subscription', () => {
const sub = makeSub();
sub.cancel();
expect(() => sub.cancel()).toThrow('Subscription đã bị hủy');
});
it('marks past due', () => {
const sub = makeSub();
sub.markPastDue();
expect(sub.status).toBe('PAST_DUE');
});
it('marks expired', () => {
const sub = makeSub();
sub.markExpired();
expect(sub.status).toBe('EXPIRED');
});
it('renews period', () => {
const sub = makeSub();
sub.markPastDue();
const newStart = new Date('2026-02-01');
const newEnd = new Date('2026-03-01');
sub.renewPeriod(newStart, newEnd);
expect(sub.status).toBe('ACTIVE');
expect(sub.currentPeriodStart).toEqual(newStart);
expect(sub.currentPeriodEnd).toEqual(newEnd);
});
});