This phase makes channelId and machine required (non-optional) throughout the entire stats pipeline, enforcing data integrity from the client through to the database layer.
Updated Files:
prisma/mongo-stats/schema/user.prismaprisma/mongo-stats/schema/user-login-history.prismaprisma/mongo-stats/schema/ads-click-history.prismaprisma/mongo-stats/schema/events.prismaChanges:
Removed ? (optional) marker from channelId field in:
User modelUserLoginHistory modelAdsClickHistory modelAdClickEvents modelVideoClickEvents modelAdImpressionEvents modelRemoved ? (optional) marker from machine field in:
User modelUserLoginHistory modelAdsClickHistory modelAdClickEvents modelVideoClickEvents modelAdImpressionEvents modelIndexes Added:
@@index([channelId, createAt]) to User for channel-based queries@@index([channelId, createAt]) to UserLoginHistory for channel-based reporting@@index([channelId, uid, clickAt]) to AdsClickHistory for channel+user analytics@@index([channelId, uid, clickAt]) to AdClickEvents for channel+user analytics@@index([channelId, uid, clickedAt]) to VideoClickEvents for channel+user analytics@@index([channelId, uid, impressionAt]) to AdImpressionEvents for channel+user analyticsUpdated File:
libs/common/src/events/user-login-event.dto.tsChanges:
// Before
export interface UserLoginEventPayload {
channelId?: string; // optional
machine?: string; // optional
}
// After
export interface UserLoginEventPayload {
channelId: string; // required
machine: string; // required
}
Updated Files:
apps/box-app-api/src/feature/auth/login.dto.tsapps/box-app-api/src/feature/ads/dto/ad-click.dto.tsapps/box-app-api/src/feature/ads/dto/ad-impression.dto.tsChanges:
LoginDto:
channelId changed from @IsOptional() to @IsNotEmpty() @IsString()`machine changed from @IsOptional() to @IsNotEmpty() @IsString()AdClickDto:
channelId changed from @IsOptional() to @IsNotEmpty() @IsString()machine changed from @IsOptional() to @IsNotEmpty() @IsString()AdImpressionDto:
channelId changed from @IsOptional() to @IsNotEmpty() @IsString()machine changed from @IsOptional() to @IsNotEmpty() @IsString()Updated File:
apps/box-stats-api/src/feature/user-login/user-login.service.tsChanges:
createUser() method:
?? null coalescing for channelId and machinerecordLogin() method:
?? null coalescing for channelId and machineUpdated File:
apps/box-stats-api/src/feature/stats-events/stats-events.consumer.tsChanges:
handleAdClick():
!payload.channelId || !payload.machine?? null coalescing, now directly assigns valueshandleVideoClick():
machine field to VideoClickMessage interface as required!payload.channelId || !payload.machine?? null coalescing, now directly assigns valueshandleAdImpression():
!payload.channelId || !payload.machine?? null coalescing, now directly assigns valuesUpdated Files:
apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.dto.tsapps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.tsChanges:
Added required channelId field to CreateChannelDto:
@IsNotEmpty()
@IsString()
@MaxLength(100)
channelId: string;
Updated channel.service.ts create method to include channelId from DTO
@prisma/mysql/client@prisma/mongo/client@prisma/mongo-stats/clientClient (Mobile/Web)
↓
POST /auth/login
├─ uid (required)
├─ channelId (required) ← NOW ENFORCED
├─ machine (required) ← NOW ENFORCED
└─ appVersion, os (optional)
↓
AuthController.login()
├─ Validates LoginDto
└─ Forwards to AuthService.login()
↓
AuthService.login()
├─ Creates UserLoginEventPayload
│ ├─ uid, channelId (required)
│ └─ machine (required)
└─ Publishes to RabbitMQ
↓
RabbitMQ (topic: user.login)
↓
UserLoginService.recordLogin()
├─ Stores UserLoginHistory with:
│ ├─ uid, channelId (required)
│ └─ machine (required)
└─ Calls createUser() to update User record
↓
MongoDB (mongo-stats database)
├─ user collection
│ ├─ id, uid, ip, os
│ ├─ channelId (required)
│ ├─ machine (required)
│ └─ indexes: [channelId, createAt], [uid, createAt]
└─ userLoginHistory collection
├─ uid, ip, userAgent, appVersion, os
├─ channelId (required)
├─ machine (required)
└─ indexes: [channelId, createAt], [uid, createAt], [ip, createAt]
---
Ad Click Flow:
↓
POST /ads/click
├─ adId (required)
├─ adType (required)
├─ channelId (required) ← NOW ENFORCED
└─ machine (required) ← NOW ENFORCED
↓
AdService.recordAdClick()
├─ Publishes StatsAdClickEventPayload
└─ RabbitMQ (stats.ad.click)
↓
StatsEventsConsumer.handleAdClick()
├─ Validates: uid, adId, channelId, machine (all required)
└─ Stores AdClickEvents with composite index [channelId, uid, clickedAt]
channelId and machine must be provided in all API requests@IsNotEmpty() and @IsString() decoratorschannelId and machineFor existing data with null channelId or machine:
# Test 1: Login with required fields
curl -X POST http://localhost:3301/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"uid": "device-123",
"channelId": "channel-us-001",
"machine": "iPhone 12 Pro"
}'
# Expected: Success, data stored with channelId and machine
# Test 2: Ad click with required fields
curl -X POST http://localhost:3301/api/v1/ads/click \
-H "Content-Type: application/json" \
-d '{
"adId": "ad-123",
"adType": "BANNER",
"channelId": "channel-us-001",
"machine": "iPhone 12 Pro"
}'
# Expected: Success, event published and persisted
# Test 3: Missing channelId (should fail)
curl -X POST http://localhost:3301/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"uid": "device-123",
"machine": "iPhone 12 Pro"
}'
# Expected: 400 Bad Request - "channelId must not be empty"
prisma/mongo-stats/schema/user.prisma ✅prisma/mongo-stats/schema/user-login-history.prisma ✅prisma/mongo-stats/schema/ads-click-history.prisma ✅prisma/mongo-stats/schema/events.prisma ✅libs/common/src/events/user-login-event.dto.ts ✅apps/box-app-api/src/feature/auth/login.dto.ts ✅apps/box-app-api/src/feature/ads/dto/ad-click.dto.ts ✅apps/box-app-api/src/feature/ads/dto/ad-impression.dto.ts ✅apps/box-stats-api/src/feature/user-login/user-login.service.ts ✅apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts ✅apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts ✅apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.dto.ts ✅✅ Prisma Generation: All three schemas passed validation ✅ TypeScript: Phase 4 critical files compile without errors
Phase 4 Status: ✅ COMPLETE Date Completed: 2025-12-09 Total Files Modified: 13 Critical Errors: 0 Warnings: Lingering Phase 3 ads-channel errors in 7 files (non-critical for Phase 4)