Skip to content

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.

ProjectPath
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.ts
  • entitlement-service/src/analytics/analytics.service.ts
  • entitlement-service/src/naluri/naluri.service.ts
  • entitlement-service/prisma/schema.prisma
  • entitlement-service/prisma/naluri-core.prisma
  • admin-api/src/focus-dashboard/focus-dashboard.service.ts
  • naluri-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 users
  • POST /analytics/manage-access - Grant user access
  • GET /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 → Features

3. 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.id

Kesimpulan

AspectFinding
couponId typeUUID (bukan string code)
PatternSama dengan Subscriptions.couponId
ReferenceHanya 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".

AspectFocus Dashboard
DomainAccess control / entitlement
Question answered"Who can access Focus Dashboard for sponsor X?"
Similar toFeature access, subscriptions
Belongs inentitlement-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 coupons table

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 couponId dan couponCodeId sebagai UUID reference
  • Tidak copy data coupon ke internal DB
  • Query Naluri Core untuk user details

Terminology

TermTableTypeExample
sponsorCodeIdcoupons.idUUID550e8400-e29b-41d4-a716-446655440000
couponCodeIdcoupon_codes.idUUID6ba7b810-9dad-11d1-80b4-00c04fd430c8
userIdusers.idUUID7c9e6679-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 Coupons dan CouponCodes model 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:

FieldSubscriptionsFocusDashboardUserAccess
couponIdString? (UUID)String (UUID)
userIdvia SubscriptionUsersdirect 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_access

Phase 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-access hanya kirim userIds[]
  • Tidak ada action field - 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 Core

Files to Modify

Entitlement-Service

FileChange
prisma/naluri-core.prismaAdd name field to Users model
prisma/schema.prismaAdd FocusDashboardUserAccess table
src/naluri/naluri.service.tsAdd searchUsers, getUsersByIds
src/analytics/analytics.module.tsImport NaluriModule
src/analytics/analytics.service.tsAdd searchUsers, getFocusDashboardUsers, manageAccess
src/analytics/analytics.controller.tsAdd 3 new endpoints
src/analytics/dto/*.tsAdd DTOs

Admin-API

FileChange
-No changes needed (proxy already correct)

Naluri-Admin (Frontend)

FileChange
src/modules/coupon/tabs/FocusDashboard.tsxRemove toggle & config fields
src/modules/coupon/api/mutations.tsSimplify manage-access payload

Benefits

  1. Correct domain placement - entitlement/access control logic in entitlement-service
  2. Consistent with existing pattern - sama seperti Subscriptions menyimpan couponId
  3. Minimal schema changes - hanya tambah 1 table internal + 1 field di naluri-core.prisma
  4. No data duplication - hanya simpan UUID references
  5. Future-proof - easier to add more entitlement features


Last updated: March 2026

Internal Documentation