Centralized database-driven banner management. Covers: marketing banners, ad placements, sponsored content, scheduled campaigns, contextual targeting, impression/click analytics, and a cacheable public serving API. All banner state is owned by the backend — no static configuration required.
| Surface | Base Path | Auth |
|---|
| Admin — Banners | /admin/banners | JWT + Banners_* permission |
| Admin — Placements | /admin/placements | JWT + Placements_* permission |
| Admin — Campaigns | /admin/campaigns | JWT + Campaigns_* permission |
| Admin — Campaign Placements | /admin/campaigns/:publicId/placements | JWT + Assignments_* permission |
| Admin — Banner Assignments | /admin/campaigns/:publicId/placements/:placementPublicId/banners | JWT + Assignments_* permission |
| Admin — Targeting Rules | /admin/targeting-rules | JWT + TargetingRules_* permission |
| Public — Serving | /banners/serve/:slug | None (@Public() class decorator) |
| Public — Tracking | /banners/events/impression, /banners/events/click | None (@Public() class decorator) |
| Action | Endpoint | Notes |
|---|
| Create banner | POST /admin/banners | Admin-only. createdBy set from JWT admin id. |
| List banners | GET /admin/banners | Paginated, filterable by isActive, scheduleStatus, brandId, categoryId, includeDeleted, search. |
| Get banner | GET /admin/banners/:publicId | |
| Update banner | PATCH /admin/banners/:publicId | Partial update; invalidates list cache on success. |
| Soft-delete banner | DELETE /admin/banners/:publicId | Sets deletedAt. Throws BANNER_ALREADY_DELETED if already deleted. |
| Restore banner | POST /admin/banners/:publicId/restore | Clears deletedAt. Throws BANNER_NOT_DELETED if not deleted. |
| Update schedule | PATCH /admin/banners/:publicId/schedule | Updates scheduleStatus, publishAt, expiresAt, recurrence window. |
| Activate | PATCH /admin/banners/:publicId/activate | Sets isActive=true. |
| Deactivate | PATCH /admin/banners/:publicId/deactivate | Sets isActive=false. |
| Action | Endpoint | Notes |
|---|
| Create placement | POST /admin/placements | Slug globally unique; layoutType enum required. |
| List placements | GET /admin/placements | Paginated, filterable. |
| Get placement | GET /admin/placements/:publicId | |
| Update placement | PATCH /admin/placements/:publicId | |
| Soft-delete placement | DELETE /admin/placements/:publicId | |
| Activate | PATCH /admin/placements/:publicId/activate | |
| Deactivate | PATCH /admin/placements/:publicId/deactivate | |
| Action | Endpoint | Notes |
|---|
| Create campaign | POST /admin/campaigns | Status is always set to draft by the service. |
| List campaigns | GET /admin/campaigns | Paginated, filterable by status, source, includeDeleted, search. |
| Get campaign | GET /admin/campaigns/:publicId | |
| Update campaign | PATCH /admin/campaigns/:publicId | |
| Soft-delete campaign | DELETE /admin/campaigns/:publicId | Throws CAMPAIGN_ALREADY_DELETED if already deleted. |
| Restore campaign | POST /admin/campaigns/:publicId/restore | Throws CAMPAIGN_NOT_DELETED if not deleted. |
| Lifecycle: draft→scheduled | PATCH /admin/campaigns/:publicId/schedule | Requires startsAt in the future. |
| Lifecycle: scheduled/active/paused→active | PATCH /admin/campaigns/:publicId/activate | |
| Lifecycle: active/paused→paused | PATCH /admin/campaigns/:publicId/pause | |
| Lifecycle: active/paused→ended | PATCH /admin/campaigns/:publicId/end | |
| Lifecycle: any non-terminal→cancelled | PATCH /admin/campaigns/:publicId/cancel | Irreversible. |
| Action | Endpoint | Notes |
|---|
| Attach placement to campaign | POST /admin/campaigns/:publicId/placements | Body: { placementPublicId, isActive? }. Creates campaign_placements row. |
| Detach placement from campaign | DELETE /admin/campaigns/:publicId/placements/:placementPublicId | |
| Activate/deactivate placement link | `PATCH /admin/campaigns/:publicId/placements/:placementPublicId/activate | deactivate` |
| List assignments | GET /admin/campaigns/:publicId/placements/:placementPublicId/banners | |
| Assign banner to slot | POST /admin/campaigns/:publicId/placements/:placementPublicId/banners | Body: AssignBannerDto. Composite-PK row. |
| Update banner assignment | PATCH /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicId | |
| Remove banner assignment | DELETE /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicId | |
| Activate/deactivate banner assignment | `PATCH /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicId/activate | deactivate` |
| Action | Endpoint | Notes |
|---|
| Add targeting rule | POST /admin/targeting-rules | Body: { campaignPublicId, ruleType, operator, value, isActive? }. |
| List targeting rules | GET /admin/targeting-rules?campaignPublicId=... | Filtered by campaign. |
| Update targeting rule | PATCH /admin/targeting-rules/:publicId | |
| Delete targeting rule | DELETE /admin/targeting-rules/:publicId | |
| Feature | Detail |
|---|
| Endpoint | GET /banners/serve/:slug |
| Rate limit | 300 req/min per IP (configurable via @IpThrottle) |
| Cache | Redis, TTL from BANNER_SERVE_CACHE_TTL_SECONDS (default 60s) |
| Cache key | banners:serve:<slug>:<ctx_hash> (single prefix; per-context segmentation) |
| Auth | None — @Public() class decorator bypasses JwtAuthGuard |
| Targeting | Evaluates all active rules for the campaign against the incoming request context. AND logic. |
| Schedule check | Filters banners by scheduleStatus + publishAt/expiresAt + recurrence window (HH:MM:SS string compare) |
| Fallback banners | Used only when primary slots unfilled AND placement.allowPartialRender=true |
| Response shape | { placement, banners[], servedAt } — servedAt is always new Date().toISOString() at serve time, NEVER cached |
| Feature | Detail |
|---|
| Impression endpoint | POST /banners/events/impression |
| Click endpoint | POST /banners/events/click |
| Rate limit | 600 req/min per IP (@IpThrottle) |
| Auth | None — @Public() class decorator |
| Response | 202 Accepted (fire-and-forget) |
| Idempotency | correlationId used as BullMQ jobId — duplicate events silently no-op |
| Event storage | Async via BullMQ BANNERS queue to banner_impressions / banner_clicks tables |
scheduleStatus is managed by admin actions and by the SYNC_SCHEDULE_STATUS cron job. Banner serving eligibility:
scheduleStatus | Eligible to serve? |
|---|
expired | Never |
evergreen | Yes (subject to recurrence window) |
scheduled | Only if publishAt <= now AND (expiresAt IS NULL OR expiresAt > now) |
active | publishAt <= now AND (expiresAt IS NULL OR expiresAt > now) |
expired is never served regardless of other conditions. The cron transitions scheduled→active and active→expired based on time.
VALID_TRANSITIONS map (module-level constant in CampaignsAdminService):
| From | Allowed → To |
|---|
draft | scheduled, active |
scheduled | active, cancelled |
active | paused, ended, cancelled |
paused | active, ended, cancelled |
ended | (none — terminal) |
cancelled | (none — terminal) |
Transitions are enforced via CAMPAIGN_INVALID_TRANSITION error. schedule requires startsAt > now AND current status in ["draft", "active", "paused"].
- All targeting rules for a campaign are evaluated with AND logic.
- A campaign with zero targeting rules passes all requests (serve to everyone).
- 9 rule types:
device, country, user_segment, login_state, language, hour_of_day, day_of_week, new_visitor, referrer_domain.
user_segment always returns true (deferred implementation).
- Targeting exceptions (e.g., rule evaluation throwing) are caught and logged as
warn — the rule is treated as passing (fail-open).
placement.maxBanners is the hard cap on banners per request.
bannerAssignments.isFallback distinguishes primary from fallback; fallbackPriority orders fallbacks.
- Primary banners (non-fallback) fill first, ordered by
displayOrder ASC, weight DESC.
- If primary slots are unfilled AND
placement.allowPartialRender=true, fallback banners fill remaining slots in fallbackPriority ASC order.
- If
allowPartialRender=false and primary slots are unfilled, the response has an empty banners array.
- Creating/updating a banner with
seoId links the banner to an SEO record. Throws BANNER_SEO_NOT_FOUND if the SEO id does not exist.
- The SEO module's
getUsage() and remove() should consider active banner references.
- Campaign budgets are stored as
bigint columns with _paisa suffix (budgetTotalPaisa, budgetDailyPaisa).
- No float or numeric types for monetary values.
- API DTOs accept and return amounts in paisa (integer).
PermissionCode format: Module_ACTION (e.g., Banners_READ). All admin endpoints use @UseGuards(JwtAuthGuard, RoleGuard) + @Permissions(...).
| Permission | Used For |
|---|
Banners_READ | List/get banners (admin) |
Banners_CREATE | Create banner |
Banners_UPDATE | Update, schedule, activate, deactivate, restore |
Banners_DELETE | Soft-delete banner |
Placements_READ / Placements_CREATE / Placements_UPDATE / Placements_DELETE | Placement admin endpoints |
Campaigns_READ / Campaigns_CREATE / Campaigns_UPDATE / Campaigns_DELETE | Campaign admin endpoints (incl. lifecycle transitions) |
Assignments_READ / Assignments_CREATE / Assignments_UPDATE / Assignments_DELETE | Campaign placements + banner assignments |
TargetingRules_READ / TargetingRules_CREATE / TargetingRules_UPDATE / TargetingRules_DELETE | Targeting rules |
| System | Direction | Purpose |
|---|
| Redis | Read/Write | Serving endpoint cache (key pattern invalidation via banners:serve:*) |
| BullMQ (Redis) | Producer + Consumer | Async impression/click writes, schedule sync (minutely), stats rollup (nightly) |
| SEO module | Read (banner create/update) | seoId FK reference |
| Admin Users | FK reference | createdBy on all admin-created entities |
| Customers | FK reference | userId on impression/click events (nullable) |