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

@@ -21,6 +21,8 @@ export const PLANS = [
priceYearlyVND: BigInt(0),
maxListings: 3,
maxSavedSearches: 5,
maxAnalyticsQueries: 0,
maxMediaUploads: 5,
features: {
basicSearch: true,
listingPost: true,
@@ -38,6 +40,8 @@ export const PLANS = [
priceYearlyVND: BigInt(4_990_000),
maxListings: 50,
maxSavedSearches: 30,
maxAnalyticsQueries: 100,
maxMediaUploads: 150,
features: {
basicSearch: true,
listingPost: true,
@@ -57,6 +61,8 @@ export const PLANS = [
priceYearlyVND: BigInt(9_990_000),
maxListings: 20,
maxSavedSearches: 100,
maxAnalyticsQueries: 500,
maxMediaUploads: 60,
features: {
basicSearch: true,
listingPost: true,
@@ -77,6 +83,8 @@ export const PLANS = [
priceYearlyVND: BigInt(49_900_000),
maxListings: null,
maxSavedSearches: null,
maxAnalyticsQueries: null,
maxMediaUploads: null,
features: {
basicSearch: true,
listingPost: true,
@@ -109,6 +117,8 @@ async function seedPlans() {
priceYearlyVND: plan.priceYearlyVND,
maxListings: plan.maxListings,
maxSavedSearches: plan.maxSavedSearches,
maxAnalyticsQueries: plan.maxAnalyticsQueries,
maxMediaUploads: plan.maxMediaUploads,
features: plan.features,
},
create: plan,