Appearance
Focus Dashboard Access - Implementation Plan
Status: Planning Created: March 2026 Decision: Option B - Implement in entitlement-service
File References
Untuk Claude: Gunakan path ini untuk akses langsung ke source code.
| Project | Path |
|---|---|
| entitlement-service | /Users/joko/Documents/projects/entitlement-service |
| admin-api | /Users/joko/Documents/projects/admin-api |
| naluri-admin | /Users/joko/Documents/projects/naluri-admin |
Key files:
entitlement-service/src/analytics/analytics.controller.tsentitlement-service/src/analytics/analytics.service.tsentitlement-service/src/naluri/naluri.service.tsentitlement-service/prisma/schema.prismaentitlement-service/prisma/naluri-core.prismaadmin-api/src/focus-dashboard/focus-dashboard.service.tsnaluri-admin/src/modules/coupon/tabs/FocusDashboard.tsx
Problem Statement
Focus Dashboard endpoints yang dipanggil dari admin-api BELUM DIIMPLEMENTASI di entitlement-service.
Endpoints yang diperlukan:
GET /analytics/search-users- Search usersPOST /analytics/manage-access- Grant user accessGET /analytics/focus-dashboard/{sponsorCodeId}- Get user access list
Architecture Analysis
Existing Pattern di Entitlement-Service
Sebelum implementasi, perlu dipahami bagaimana entitlement-service sudah handle coupon:
1. Subscriptions menyimpan couponId sebagai UUID:
prisma
// entitlement-service/prisma/schema.prisma
model Subscriptions {
couponId String? @map("coupon_id") // UUID reference ke coupons.id
}2. Flow feature access saat ini:
User email
↓
NaluriService.getCouponIdByUserEmail() → Get users.coupon_id (UUID)
↓
SubscriptionsService.getByCouponId(couponId) → Match dengan Subscriptions.couponId
↓
Plans → Features3. Di Naluri Core, users table menyimpan:
typescript
// admin-api/src/members/entities/member.entity.ts
coupon_id: string; // UUID → references coupons.id
code_id: string; // UUID → references coupon_codes.idKesimpulan
| Aspect | Finding |
|---|---|
couponId type | UUID (bukan string code) |
| Pattern | Sama dengan Subscriptions.couponId |
| Reference | Hanya simpan ID, tidak copy data coupon |
Proposal ini KONSISTEN dengan pattern existing.
Why Entitlement-Service?
Focus Dashboard adalah access control - "siapa yang entitled untuk akses dashboard".
| Aspect | Focus Dashboard |
|---|---|
| Domain | Access control / entitlement |
| Question answered | "Who can access Focus Dashboard for sponsor X?" |
| Similar to | Feature access, subscriptions |
| Belongs in | entitlement-service |
Design Decisions
1. No isEnabled toggle
- By default semua coupon code enabled
- Tidak perlu toggle
2. Move clientName, textlineNumber, country ke Coupon Detail
- Field ini properti coupon, bukan Focus Dashboard
- Simpan di
couponstable
3. No revoke action
- Hanya kirim
userIds[] - Empty array = hapus semua access untuk couponCodeId tersebut
- Tidak ada user = tidak ada access
4. User response includes name
- Get dari users table di Naluri Core
5. Consistent with existing pattern
- Simpan
couponIddancouponCodeIdsebagai UUID reference - Tidak copy data coupon ke internal DB
- Query Naluri Core untuk user details
Terminology
| Term | Table | Type | Example |
|---|---|---|---|
sponsorCodeId | coupons.id | UUID | 550e8400-e29b-41d4-a716-446655440000 |
couponCodeId | coupon_codes.id | UUID | 6ba7b810-9dad-11d1-80b4-00c04fd430c8 |
userId | users.id | UUID | 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Implementation (Option B - entitlement-service)
Phase 1: Database Schema
1.1 Extend Users model in naluri-core.prisma (add name field)
prisma
// /Users/joko/Documents/projects/entitlement-service/prisma/naluri-core.prisma
model Users {
id String @id
email String
name String? // ADD THIS - untuk response user access
couponId String? @map("coupon_id")
@@map("users")
}Note: Tidak perlu tambah
CouponsdanCouponCodesmodel karena kita hanya simpan UUID reference, tidak perlu query coupon details.
1.2 Add FocusDashboardUserAccess to schema.prisma (internal DB)
prisma
// /Users/joko/Documents/projects/entitlement-service/prisma/schema.prisma
model FocusDashboardUserAccess {
id String @id @default(uuid())
couponId String @map("coupon_id") // UUID → coupons.id from Naluri Core
couponCodeId String @map("coupon_code_id") // UUID → coupon_codes.id from Naluri Core
userId String @map("user_id") // UUID → users.id from Naluri Core
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([couponId, couponCodeId, userId])
@@index([couponId])
@@index([userId])
@@map("focus_dashboard_user_access")
}Pattern comparison dengan Subscriptions:
| Field | Subscriptions | FocusDashboardUserAccess |
|---|---|---|
| couponId | String? (UUID) | String (UUID) |
| userId | via SubscriptionUsers | direct field |
| couponCodeId | - | String (UUID) |
1.3 Run migrations
bash
cd /Users/joko/Documents/projects/entitlement-service
yarn prisma generate --schema=prisma/naluri-core.prisma
yarn prisma generate
yarn prisma migrate dev --name add_focus_dashboard_user_accessPhase 2: Service Layer
2.1 Extend NaluriService
File: /Users/joko/Documents/projects/entitlement-service/src/naluri/naluri.service.ts
typescript
// Add to existing NaluriService
// Search users by email
async searchUsers(email: string, type: 'nalurian' | 'member') {
const whereClause: any = {
email: { contains: email, mode: 'insensitive' },
};
if (type === 'nalurian') {
whereClause.email = { ...whereClause.email, endsWith: '@naluri.life' };
} else {
whereClause.NOT = { email: { endsWith: '@naluri.life' } };
}
return this.db.users.findMany({
where: whereClause,
select: { id: true, email: true, name: true },
take: 20,
});
}
// Get users by IDs
async getUsersByIds(ids: string[]) {
return this.db.users.findMany({
where: { id: { in: ids } },
select: { id: true, email: true, name: true },
});
}2.2 Add Focus Dashboard methods to AnalyticsService
File: /Users/joko/Documents/projects/entitlement-service/src/analytics/analytics.service.ts
typescript
@Injectable()
export class AnalyticsService {
constructor(
private readonly db: DatabaseService,
private readonly naluriService: NaluriService,
) {}
// Search users from Naluri Core
async searchUsers(email: string, type: 'nalurian' | 'member') {
return this.naluriService.searchUsers(email, type);
}
// Get users with access for a sponsor code
async getFocusDashboardUsers(sponsorCodeId: string) {
// Get access records
const accessRecords = await this.db.focusDashboardUserAccess.findMany({
where: { couponId: sponsorCodeId, deletedAt: null },
});
if (accessRecords.length === 0) {
return { sponsorCodeId, users: [] };
}
// Get unique user IDs
const userIds = [...new Set(accessRecords.map(r => r.userId))];
// Get user details from Naluri Core
const users = await this.naluriService.getUsersByIds(userIds);
// Build user map with couponCodeIds
const userMap = new Map<string, { id: string; email: string; name: string; couponCodeIds: string[] }>();
for (const user of users) {
userMap.set(user.id, {
id: user.id,
email: user.email,
name: user.name || '',
couponCodeIds: [],
});
}
for (const record of accessRecords) {
if (userMap.has(record.userId)) {
userMap.get(record.userId).couponCodeIds.push(record.couponCodeId);
}
}
return {
sponsorCodeId,
users: Array.from(userMap.values()),
};
}
// Set user access (replace all for a couponCodeId)
async manageAccess(dto: ManageAccessDto) {
// Soft delete existing records for this coupon+couponCode
await this.db.focusDashboardUserAccess.updateMany({
where: {
couponId: dto.sponsorCodeId,
couponCodeId: dto.couponCodeId,
deletedAt: null,
},
data: { deletedAt: new Date() },
});
// Insert new records
if (dto.userIds.length > 0) {
await this.db.focusDashboardUserAccess.createMany({
data: dto.userIds.map(userId => ({
couponId: dto.sponsorCodeId,
couponCodeId: dto.couponCodeId,
userId,
})),
skipDuplicates: true,
});
}
return { success: true };
}
}Phase 3: API Endpoints
File: /Users/joko/Documents/projects/entitlement-service/src/analytics/analytics.controller.ts
typescript
@Controller('analytics')
export class AnalyticsController {
constructor(private readonly analyticsService: AnalyticsService) {}
@Get('search-users')
async searchUsers(@Query() query: SearchUsersQueryDto) {
return this.analyticsService.searchUsers(query.email, query.type);
}
@Get('focus-dashboard/:sponsorCodeId')
async getFocusDashboardUsers(@Param('sponsorCodeId') sponsorCodeId: string) {
return this.analyticsService.getFocusDashboardUsers(sponsorCodeId);
}
@Post('manage-access')
async manageAccess(@Body() dto: ManageAccessDto) {
return this.analyticsService.manageAccess(dto);
}
}DTOs:
typescript
// /Users/joko/Documents/projects/entitlement-service/src/analytics/dto/
// search-users.dto.ts
export class SearchUsersQueryDto {
@IsString()
email: string;
@IsIn(['nalurian', 'member'])
type: 'nalurian' | 'member';
}
// manage-access.dto.ts
export class ManageAccessDto {
@IsUUID()
sponsorCodeId: string;
@IsUUID()
couponCodeId: string;
@IsArray()
@IsUUID('4', { each: true })
userIds: string[]; // Empty array = remove all access
}
// focus-dashboard-response.dto.ts
export class FocusDashboardUserDto {
id: string;
email: string;
name: string;
couponCodeIds: string[];
}
export class FocusDashboardResponseDto {
sponsorCodeId: string;
users: FocusDashboardUserDto[];
}Phase 4: Update Analytics Module
File: /Users/joko/Documents/projects/entitlement-service/src/analytics/analytics.module.ts
typescript
@Module({
imports: [NaluriModule], // Add this import
controllers: [AnalyticsController],
providers: [AnalyticsService],
})
export class AnalyticsModule {}Phase 5: Frontend Changes
File: /Users/joko/Documents/projects/naluri-admin/src/modules/coupon/tabs/FocusDashboard.tsx
- Remove enable/disable toggle
- Remove clientName, textlineNumber, country fields
- Keep only user access management (GrantAccessSelector)
API contract update:
POST /focus-dashboard/manage-accesshanya kirimuserIds[]- Tidak ada
actionfield - always replace
API Flow
naluri-admin admin-api entitlement-service
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ FocusDashboard │ │ focus-dashboard│ │ analytics │
│ .tsx │ │ .service.ts │ │ .service.ts │
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
│ GET /focus-dashboard/ │ GET /analytics/ │
│ details/{sponsorCodeId} ───▶ │ focus-dashboard/{id} ───▶ │ Query internal DB
│ │ │ + Naluri Core (users)
│ │ │
│ POST /focus-dashboard/ │ POST /analytics/ │
│ manage-access ───▶ │ manage-access ───▶ │ Soft delete + insert
│ │ │
│ GET /focus-dashboard/ │ GET /analytics/ │
│ search-users ───▶ │ search-users ───▶ │ Query Naluri CoreFiles to Modify
Entitlement-Service
| File | Change |
|---|---|
prisma/naluri-core.prisma | Add name field to Users model |
prisma/schema.prisma | Add FocusDashboardUserAccess table |
src/naluri/naluri.service.ts | Add searchUsers, getUsersByIds |
src/analytics/analytics.module.ts | Import NaluriModule |
src/analytics/analytics.service.ts | Add searchUsers, getFocusDashboardUsers, manageAccess |
src/analytics/analytics.controller.ts | Add 3 new endpoints |
src/analytics/dto/*.ts | Add DTOs |
Admin-API
| File | Change |
|---|---|
| - | No changes needed (proxy already correct) |
Naluri-Admin (Frontend)
| File | Change |
|---|---|
src/modules/coupon/tabs/FocusDashboard.tsx | Remove toggle & config fields |
src/modules/coupon/api/mutations.ts | Simplify manage-access payload |
Benefits
- Correct domain placement - entitlement/access control logic in entitlement-service
- Consistent with existing pattern - sama seperti Subscriptions menyimpan couponId
- Minimal schema changes - hanya tambah 1 table internal + 1 field di naluri-core.prisma
- No data duplication - hanya simpan UUID references
- Future-proof - easier to add more entitlement features
Related Documentation
Last updated: March 2026