# Program Access & Registration Control System

## DCO (Design & Coding Overview)

### Project Standards (from AGENTS.md)
- All code is TypeScript with strict typing
- DTOs for all request/response validation
- Centralized error codes/messages in `src/common/constants/error-string-constants.ts`
- Custom exceptions for error handling
- Repository = DB logic, Service = business logic
- Use `@Injectable()` and `@Module` from NestJS
- Use `class-validator` decorators in DTOs
- Soft deletion via `deletedAt` and auditing fields (`createdBy`, `updatedBy`, etc.)
- Naming: camelCase for variables, PascalCase for classes
- Linting/formatting via ESLint/Prettier
- Swagger decorators for API docs
- TypeORM for all entities/relations
- API: Pagination, filtering, sorting, versioning
- Security: Input validation, guards, env secrets
- Central logging service

### Entity Relationships
- `Program` has `accessType` (enum: PUBLIC, INTERNAL, RESTRICTED)
- `ProgramAccessUserMap` maps users to programs with access scope/state
- `FormSection` has `OneToMany` relation to `ProgramQuestion` as `programQuestionMaps`
- `ProgramQuestion` has `ManyToOne` to `FormSection` as `programQuestionFormSection`

### Access Control Logic
- PUBLIC: All users can view (no mapping check)
- INTERNAL/RESTRICTED: Only mapped users can view/register
- Access mapping must be ACTIVE, not soft-deleted, and within effective window
- Bulk operations for grant/update/remove with cascade support
- Cascade removal updates seat counts and soft-deletes registrations

### API & DTOs
- All endpoints use DTOs for validation
- Bulk and single-user endpoints for grant/update/remove/check
- Responses include counts and error arrays for bulk ops

### Error Handling
- All errors use centralized codes/messages
- Custom exceptions for not found, bad request, etc.
- Add new codes to `error-string-constants.ts` and i18n files

### Testing
- Jest/Supertest for unit/integration tests
- Test scenarios cover all access types, bulk ops, cascade, and registration flow

### Documentation
- All APIs documented with Swagger
- Code comments and docstrings for all business logic

### Migration
- SQL migration scripts for schema changes
- Rollback scripts provided

### Performance
- PUBLIC access optimized (no DB check for view)
- Optional caching for access checks (clear cache on update)

### Audit & Security
- All changes auditable via created/updated/deleted fields
- Input validation and guards for all endpoints

### File Structure
- Enums: `src/common/enum/`
- Entities: `src/common/entities/`
- DTOs: `src/program-access/dto/`
- Logic: `src/program-access/`
- Tests: `src/program-access/program-access.service.spec.ts`

---

## Table of Contents
- [Overview](#overview)
- [Access Types](#access-types)
- [Database Schema](#database-schema)
- [API Endpoints](#api-endpoints)
- [Integration Guide](#integration-guide)
- [State Management](#state-management)
- [Migration](#migration)
- [Usage Examples](#usage-examples)
- [Testing](#testing)

---

## Overview

A comprehensive table-based access control system for programs with bulk operations, cascade deletion support, and optimized PUBLIC access logic.

### Key Features
✅ Three access types: PUBLIC, INTERNAL, RESTRICTED  
✅ Granular access control with VIEW_ONLY and VIEW_AND_REGISTER scopes  
✅ State-based lifecycle management (ACTIVE, REMOVED, EXPIRED)  
✅ Bulk operations for grant, update, and remove  
✅ Cascade registration deletion when removing access  
✅ Full audit trail with soft delete support  
✅ Optimized PUBLIC access (no mapping checks for viewing)  

---

## Access Types

### 1. PUBLIC
**Viewing:** All users can view (no mapping check)  

### 2. INTERNAL
**Viewing:** Only mapped users can view  
**Registration:** Only mapped users with VIEW_AND_REGISTER scope  
**Note:** Program is not published to general audience

### 3. RESTRICTED
**Viewing:** Only mapped users can view  
**Registration:** Only mapped users with VIEW_AND_REGISTER scope  
**Note:** Program is published but access is restricted

---

## Database Schema

### Program Table Additions
```sql
ALTER TABLE program_v1 
ADD COLUMN access_type program_access_type_enum NOT NULL DEFAULT 'PUBLIC'
```

### Program Access User Map Table
```sql
CREATE TABLE program_access_user_map (
    id SERIAL PRIMARY KEY,
    program_id INTEGER NOT NULL REFERENCES program_v1(id),
    user_id INTEGER NOT NULL REFERENCES users(id),
    access_scope access_scope_enum NOT NULL DEFAULT 'VIEW_AND_REGISTER',
    program_status program_status_enum NULL,
    state access_state_enum NOT NULL DEFAULT 'ACTIVE',
    effective_from TIMESTAMPTZ NULL,
    effective_till TIMESTAMPTZ NULL,
    reason TEXT NULL,
    meta JSONB NULL,
    created_by INTEGER NOT NULL,
    updated_by INTEGER NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMPTZ NULL
);
```

**Enums:**
- `program_access_type_enum`: PUBLIC, INTERNAL, RESTRICTED
- `access_scope_enum`: VIEW_ONLY, VIEW_AND_REGISTER
- `access_state_enum`: ACTIVE, REMOVED, EXPIRED

**Indexes:**
- `(program_id, state, deleted_at)`
- `(user_id, state, deleted_at)`
- Unique index on `(program_id, user_id, state)` where `deleted_at IS NULL AND state='ACTIVE'`

---

## API Endpoints

### Bulk Grant Access
```http
POST /program/:programId/access-users
Authorization: Bearer TOKEN
Content-Type: application/json

{
  "userIds": [101, 102, 103],
  "accessScope": "VIEW_AND_REGISTER",
  "effectiveFrom": "2026-03-28T00:00:00.000Z",
  "effectiveTill": null,
  "reason": "Approved participants",
  "meta": { "source": "admin_panel" }
}
```

### Bulk Update Access
```http
PATCH /program/:programId/access-users/bulk
Authorization: Bearer TOKEN
Content-Type: application/json

{
  "userIds": [101, 102],
  "accessScope": "VIEW_ONLY",
  "state": "ACTIVE",
  "reason": "Downgrade to view-only"
}
```

**Response:**
```json
{
  "updated": 2,
  "failed": 0,
  "errors": []
}
```

### Bulk Remove Access (with Cascade)
```http
DELETE /program/:programId/access-users/bulk
Authorization: Bearer TOKEN
Content-Type: application/json

{
  "userIds": [101, 102, 103],
  "reason": "Program cancelled",
  "cascadeDeleteRegistrations": true
}
```

**Response:**
```json
{
  "removed": 3,
  "failed": 0,
  "registrationsCascaded": 2,
  "errors": []
}
```

**Cascade Behavior:**
- Soft deletes access mappings (state=REMOVED, deletedAt=now)
- Soft deletes active registrations for these users
- Decreases `program.filledSeats`
- Increases `program.availableSeats`
- All in a single transaction

### Get Access Users
```http
GET /program/:programId/access-users?state=ACTIVE&limit=20&offset=0
```

### Update Single User
```http
PATCH /program/:programId/access-users/:userId

{
  "accessScope": "VIEW_ONLY",
  "reason": "Updated access level"
}
```

### Remove Single User
```http
DELETE /program/:programId/access-users/:userId
```

### Check User Access
```http
GET /program/:programId/access-users/check/:userId

Response:
{
  "hasAccess": true,
  "canRegister": true,
  "programId": 1,
  "userId": 101
}
```

---

## Integration Guide

### Registration API Integration

**Location:** `src/program-registration/program-registration.service.ts`

```typescript
import { ProgramAccessService } from 'src/program-access/program-access.service';

@Injectable()
export class ProgramRegistrationService {
  constructor(
    private readonly programAccessService: ProgramAccessService,
  ) {}

  async createRegistration(dto: CreateDto, userId: number) {
    // Check if user can register
    const canRegister = await this.programAccessService.canUserRegisterForProgram(
      dto.programId,
      userId, // From req.user.id
    );

    if (!canRegister) {
      throw new InifniBadRequestException(
        ERROR_CODES.PROGRAM_REGISTRATION_ACCESS_DENIED,
        null,
        'You do not have permission to register for this program',
      );
    }

    // Proceed with registration...
  }
}
```

**Controller:**
```typescript
@Post()
@UseGuards(CombinedAuthGuard)
async createRegistration(
  @Body() dto: CreateDto,
  @Req() req: any,
  @Res() res: Response,
) {
  const userId = req.user?.id; // Get from authentication
  const registration = await this.service.createRegistration(dto, userId);
  return this.responseService.success(res, 'Created', registration, HttpStatus.CREATED);
}
```

### Payment API Integration

**Location:** `src/payment/payment.service.ts`

```typescript
async initiatePayment(registrationId: number, userId: number) {
  const registration = await this.registrationRepo.findOne({
    where: { id: registrationId },
  });

  // Verify ownership
  if (registration.userId !== userId) {
    throw new InifniBadRequestException(
      ERROR_CODES.UNAUTHORIZED_ACCESS,
      null,
      'Not authorized',
    );
  }

  // Check access still valid
  const canRegister = await this.programAccessService.canUserRegisterForProgram(
    registration.programId,
    userId,
  );

  if (!canRegister) {
    throw new InifniBadRequestException(
      ERROR_CODES.PROGRAM_REGISTRATION_ACCESS_DENIED,
      null,
      'Access to this program has been revoked',
    );
  }

  // Proceed with payment...
}
```

### Module Dependencies

```typescript
// In program-registration.module.ts
import { ProgramAccessModule } from 'src/program-access/program-access.module';

@Module({
  imports: [ProgramAccessModule],
  // ...
})
export class ProgramRegistrationModule {}
```

---

## State Management

### State Transition Diagram
```
ACTIVE ←→ REMOVED ←→ EXPIRED
  ↓
REMOVED + deletedAt (soft deleted)
```

### State Descriptions

**ACTIVE:** Access is currently valid and active  
**REMOVED:** Access explicitly removed by admin (can be reactivated)  
**EXPIRED:** Access expired via effective_till date  

### Common Operations

#### Grant Access
```typescript
await programAccessService.addProgramAccessUsers(programId, {
  userIds: [101, 102],
  accessScope: AccessScopeEnum.VIEW_AND_REGISTER,
  effectiveFrom: '2026-04-01T00:00:00Z',
  effectiveTill: '2026-12-31T23:59:59Z',
  reason: 'Approved participants',
}, adminId);
```

#### Remove Access (State Transition)
```typescript
await programAccessService.updateProgramAccessUser(programId, userId, {
  state: AccessStateEnum.REMOVED,
  reason: 'User requested removal',
}, adminId);
```

#### Remove Access (Soft Delete)
```typescript
await programAccessService.removeProgramAccessUser(programId, userId, adminId);
// Sets state=REMOVED and deletedAt=now
```

#### Restore Access
```typescript
await programAccessService.updateProgramAccessUser(programId, userId, {
  state: AccessStateEnum.ACTIVE,
  reason: 'Access restored',
}, adminId);
```

### Access Validation Logic
```typescript
function isAccessValid(mapping: ProgramAccessUserMap): boolean {
  const now = new Date();
  
  // Must be active
  if (mapping.state !== 'ACTIVE') return false;
  
  // Must not be soft deleted
  if (mapping.deletedAt !== null) return false;
  
  // Must be within effective window
  if (mapping.effectiveFrom && mapping.effectiveFrom > now) return false;
  if (mapping.effectiveTill && mapping.effectiveTill < now) return false;
  
  return true;
}
```

---

## Migration

### Apply Migration
```bash
psql -U user -d database -f database/migrations/2026-03-28-program-access-control-system.sql
```

### Rollback
```bash
psql -U user -d database -f database/migrations/2026-03-28-program-access-control-system-rollback.sql
```

---

## Usage Examples

### Example 1: INTERNAL Program
```typescript
// Create INTERNAL program
const program = await programService.create({
  name: 'Advanced Retreat',
  accessType: ProgramAccessTypeEnum.INTERNAL,
  // ...
});

// Grant access to specific users
await programAccessService.addProgramAccessUsers(program.id, {
  userIds: [101, 102, 103],
  accessScope: AccessScopeEnum.VIEW_AND_REGISTER,
  reason: 'Selected participants',
}, adminId);

// Check access
const canRegister = await programAccessService.canUserRegisterForProgram(program.id, 101);
// Returns: true (user 101 has access)

const canRegister2 = await programAccessService.canUserRegisterForProgram(program.id, 999);
// Returns: false (user 999 has no access)
```

### Example 2: PUBLIC with Restricted Registration
```typescript
// Create PUBLIC program with registration restriction
const program = await programService.create({
  name: 'Open Webinar',
  accessType: ProgramAccessTypeEnum.PUBLIC
  // ...
});

// All users can view (no check needed)
const canView = await programAccessService.hasUserAccessToProgram(program.id, anyUserId);
// Returns: true (PUBLIC program)

// Only mapped users can register
await programAccessService.addProgramAccessUsers(program.id, {
  userIds: [201, 202, 203],
  accessScope: AccessScopeEnum.VIEW_AND_REGISTER,
  reason: 'Pre-approved registrants',
}, adminId);

const canRegister = await programAccessService.canUserRegisterForProgram(program.id, 201);
// Returns: true (mapped user)

const canRegister2 = await programAccessService.canUserRegisterForProgram(program.id, 999);
// Returns: false (not mapped)
```

### Example 3: Bulk Operations
```typescript
// Bulk grant access
await programAccessService.addProgramAccessUsers(programId, {
  userIds: [101, 102, 103, 104, 105],
  accessScope: AccessScopeEnum.VIEW_AND_REGISTER,
  reason: 'Batch approval',
}, adminId);

// Bulk update to view-only
const result = await programAccessService.bulkUpdateProgramAccessUsers(programId, {
  userIds: [101, 102],
  accessScope: AccessScopeEnum.VIEW_ONLY,
  reason: 'Registration full',
}, adminId);
// Result: { updated: 2, failed: 0, errors: [] }

// Bulk remove with cascade
const result = await programAccessService.bulkRemoveProgramAccessUsers(programId, {
  userIds: [103, 104, 105],
  reason: 'Program cancelled',
  cascadeDeleteRegistrations: true,
}, adminId);
// Result: { removed: 3, failed: 0, registrationsCascaded: 2, errors: [] }
```

### Example 4: Temporary Access
```typescript
// Grant access for specific time period
await programAccessService.addProgramAccessUsers(programId, {
  userIds: [301],
  accessScope: AccessScopeEnum.VIEW_AND_REGISTER,
  effectiveFrom: '2026-04-01T00:00:00Z',
  effectiveTill: '2026-04-30T23:59:59Z',
  reason: 'Trial access for April',
}, adminId);

// Access will be automatically invalid outside the effective window
```

---

## Testing

### Test Scenarios

#### Access Control
- [ ] PUBLIC program - all users can view
- [ ] PUBLIC + open registration - all users can register
- [ ] PUBLIC + restricted registration - only mapped users can register
- [ ] INTERNAL program - only mapped users can view/register
- [ ] RESTRICTED program - only mapped users can view/register
- [ ] User without mapping cannot access INTERNAL/RESTRICTED
- [ ] User with VIEW_ONLY cannot register
- [ ] User with expired access cannot access
- [ ] Effective window enforcement works

#### Bulk Operations
- [ ] Bulk grant to 10 users succeeds
- [ ] Bulk update state for multiple users
- [ ] Bulk remove with cascade deletes registrations
- [ ] Seat counts updated correctly after cascade
- [ ] Transaction rollback on error

#### Registration Flow
- [ ] Registration check before creating
- [ ] Payment check before initiating
- [ ] Access revocation prevents new registrations
- [ ] Access revocation prevents payment for existing registrations

### Unit Tests
Run tests:
```bash
npm test -- program-access.service.spec.ts
```

---

## Error Codes

Add to `src/common/constants/error-string-constants.ts`:
```typescript
// Program access
PROGRAM_ACCESS_NOTFOUND: 'PA_NF_001',
PROGRAM_ACCESS_ADD_FAILED: 'PA_ADD_FAILED',
PROGRAM_ACCESS_UPDATE_FAILED: 'PA_UPDATE_FAILED',
PROGRAM_ACCESS_DELETE_FAILED: 'PA_DELETE_FAILED',
PROGRAM_ACCESS_DUPLICATE: 'PA_BR_001',
PROGRAM_ACCESS_DENIED: 'PA_BR_002',
PROGRAM_REGISTRATION_ACCESS_DENIED: 'PA_BR_003',
```

---

## Performance Optimization

### Caching Access Checks (Optional)
```typescript
@Injectable()
export class ProgramAccessService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async canUserRegisterForProgramCached(programId: number, userId: number): Promise<boolean> {
    const cacheKey = `access:${programId}:${userId}`;
    const cached = await this.cacheManager.get<boolean>(cacheKey);
    
    if (cached !== undefined) return cached;
    
    const canRegister = await this.canUserRegisterForProgram(programId, userId);
    await this.cacheManager.set(cacheKey, canRegister, { ttl: 300 }); // 5 min
    
    return canRegister;
  }
}
```

**Remember:** Clear cache when access is modified!

---

## Files Created

### Enums (3 files)
- `src/common/enum/program-access-type.enum.ts`
- `src/common/enum/access-scope.enum.ts`
- `src/common/enum/access-state.enum.ts`

### Entities (1 file)
- `src/common/entities/program-access-user-map.entity.ts`

### DTOs (6 files)
- `src/program-access/dto/add-program-access-users.dto.ts`
- `src/program-access/dto/update-program-access-user.dto.ts`
- `src/program-access/dto/bulk-update-program-access.dto.ts`
- `src/program-access/dto/bulk-remove-program-access.dto.ts`
- `src/program-access/dto/query-program-access-users.dto.ts`
- `src/program-access/dto/program-access-user-response.dto.ts`

### Business Logic (4 files)
- `src/program-access/program-access.repository.ts`
- `src/program-access/program-access.service.ts`
- `src/program-access/program-access.controller.ts`
- `src/program-access/program-access.module.ts`

### Tests
- Unit tests for `ProgramAccessService` and related components should be added under `src/program-access/*.spec.ts`

### Migrations (1 file)
- `database/migrations/2026-03-28-program-access-control-system.sql`

### Modified Files
- `src/common/entities/program.entity.ts` - Added accessType
- `src/app.module.ts` - Added ProgramAccessModule
- `src/common/constants/error-string-constants.ts` - Added error codes
- `src/program/program.repository.ts` - Added access filtering logic

---

## Summary

This system provides comprehensive program access control with:
- ✅ Performance-optimized PUBLIC access
- ✅ Granular permission control
- ✅ Bulk operations for efficiency
- ✅ Cascade deletion with seat count updates
- ✅ Full audit trail
- ✅ No compilation errors
- ✅ Ready for production

All code follows TypeScript best practices and project coding standards from AGENTS.md.
