feat(subscriptions): implement subscription quota enforcement

- Apply QuotaGuard + @RequireQuota to listing creation and analytics endpoints
- Add QuotaExceeded domain event emitted when quota is exceeded
- Create ListingCreatedUsageHandler to auto-meter usage on listing creation
- Create QuotaExceededListener to send email notifications on quota exceeded
- Add maxAnalyticsQueries and maxMediaUploads fields to Plan model
- Add quota.exceeded email notification template
- Define quota limits per plan tier in seed data
- Add 15 unit tests covering guard, event handler, listener, and event

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 14:16:32 +07:00
parent 23bb380d34
commit 3864f78405
17 changed files with 474 additions and 6 deletions

View File

@@ -0,0 +1,21 @@
import { QuotaExceededEvent } from '../events/quota-exceeded.event';
describe('QuotaExceededEvent', () => {
it('creates event with correct properties', () => {
const event = new QuotaExceededEvent('user-1', 'listings_created', 3, 3);
expect(event.eventName).toBe('quota.exceeded');
expect(event.aggregateId).toBe('user-1');
expect(event.metric).toBe('listings_created');
expect(event.limit).toBe(3);
expect(event.used).toBe(3);
expect(event.occurredAt).toBeInstanceOf(Date);
});
it('creates event for analytics metric', () => {
const event = new QuotaExceededEvent('user-2', 'analytics_queries', 0, 0);
expect(event.metric).toBe('analytics_queries');
expect(event.limit).toBe(0);
});
});

View File

@@ -0,0 +1,13 @@
import { type DomainEvent } from '@modules/shared/domain/domain-event';
export class QuotaExceededEvent implements DomainEvent {
readonly eventName = 'quota.exceeded';
readonly occurredAt = new Date();
constructor(
public readonly aggregateId: string, // userId
public readonly metric: string,
public readonly limit: number,
public readonly used: number,
) {}
}