# QR Attendance & Check-In

**Module:** `src/qr-attendance/`  
**Base route:** `/qr-attendance`  
**Auth:** Bearer token + `activeRole` header required on all endpoints

---

## Table of Contents

1. [Overview](#1-overview)
2. [API Reference](#2-api-reference)
3. [QR Generation](#3-qr-generation)
   - [Single QR](#31-single-qr)
   - [Bulk QR](#32-bulk-qr)
   - [Eligibility Rules](#33-eligibility-rules)
4. [Check-In Flows](#4-check-in-flows)
   - [Scan QR](#41-scan-qr)
   - [Manual Check-In](#42-manual-check-in)
   - [Undo Check-In](#43-undo-check-in)
5. [List & Download](#5-list--download)
6. [Data Model](#6-data-model)
7. [Error Codes](#7-error-codes)
8. [Key Files](#8-key-files)

---

## 1. Overview

The QR Attendance module manages end-to-end attendance tracking for program registrations:

- **Generate** a unique QR code per registration (single or bulk)
- **Check in** a registrant by scanning their QR code or manually
- **Undo** a check-in if marked incorrectly
- **List & export** attendance data with filters, pagination, and Excel download

Each QR code encodes the `registrationId`. Scanning it calls the `/scan` endpoint which looks up the attendance record and marks the registrant as attended.

---

## 2. API Reference

| Method | Endpoint | Role | Description |
|--------|----------|------|-------------|
| `POST` | `/qr-attendance/generate` | `admin` | Generate QR for a single registration |
| `POST` | `/qr-attendance/generate-bulk` | `admin` | Start bulk QR generation (async) |
| `GET` | `/qr-attendance/generate-bulk/status/:jobId` | `admin` | Poll bulk job progress |
| `POST` | `/qr-attendance/scan` | `admin`, `operational_manager` | Check in via QR scan |
| `POST` | `/qr-attendance/manual-checkin` | `admin` | Manually mark attendance |
| `POST` | `/qr-attendance/undo-checkin` | `admin` | Revert check-in(s) |
| `GET` | `/qr-attendance` | `admin` | List attendance records |
| `GET` | `/qr-attendance/:id` | `admin` | Get single attendance record |

---

## 3. QR Generation

### 3.1 Single QR

**`POST /qr-attendance/generate`**

Generates a QR code for one registration. Safe to call multiple times — if a QR already exists for the registration it returns the existing record without creating a duplicate.

**Request body:**

```json
{
  "registrationId": 101,
  "sessionId": 5
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `registrationId` | Yes | ID of the program registration |
| `sessionId` | No | Links the attendance record to a specific session |

**Flow:**

```
1. Fetch registration (with user, program, paymentDetails, rmContactUser)
2. Validate eligibility → 400 if ineligible
3. Check if QR already exists → return existing record if yes
4. Generate QR image (JPEG) encoding registrationId
5. Upload to S3: assets/qr-attendance-portal/<timestamp>/<registrationId>_<timestamp>.jpeg
6. Insert ProgramUserAttendance row (isAttended = false)
7. Return attendance record
```

**Response (201):**

```json
{
  "success": true,
  "data": {
    "id": 55,
    "registrationId": 101,
    "qrUrl": "https://s3.../assets/qr-attendance-portal/.../101_....jpeg",
    "isAttended": false,
    "isManuallyCheckedIn": false,
    "checkedInAt": null
  }
}
```

---

### 3.2 Bulk QR

**`POST /qr-attendance/generate-bulk`**

Queues QR generation for all eligible registrations in a program that do not already have a QR. Returns immediately with a `jobId` — processing runs in the background.

**Request body:**

```json
{
  "programId": 1082,
  "sessionId": 5,
  "batchSize": 10
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `programId` | Yes | Program to generate QRs for |
| `sessionId` | No | Links all generated records to this session |
| `batchSize` | No | Registrations per batch (default: `10`, max: `50`) |

**Response (202):**

```json
{
  "success": true,
  "data": {
    "jobId": 42,
    "status": "processing",
    "total": 150,
    "folderName": "assets/qr-attendance-portal/20260521143000",
    "s3Url": "https://s3.../assets/qr-attendance-portal/..."
  }
}
```

**Poll status — `GET /qr-attendance/generate-bulk/status/:jobId`**

```json
{
  "success": true,
  "data": {
    "id": 42,
    "status": "completed",
    "total": 150,
    "generated": 147,
    "skipped": 3,
    "failed": 0,
    "completedAt": "2026-05-21T14:32:10.000Z"
  }
}
```

| `status` | Meaning |
|----------|---------|
| `processing` | Job is running |
| `completed` | All batches finished |
| `failed` | Job encountered a fatal error |

- **`skipped`** — registration already had a QR, or became ineligible between job start and processing
- **`failed`** — individual registration threw an error (others continue)

---

### 3.3 Eligibility Rules

A registration is eligible for QR generation when:

```
registrationStatus  NOT IN  [REJECTED, SAVE_AS_DRAFT]

AND (
    program.requiresPayment = false
    OR registration.isFreeSeat = true
    OR paymentDetails contains at least one row with
       paymentStatus IN [ONLINE_COMPLETED, OFFLINE_COMPLETED]
)
```

Registrations that fail this check are silently skipped during bulk generation.

---

## 4. Check-In Flows

### 4.1 Scan QR

**`POST /qr-attendance/scan`**

Called when an admin scans a registrant's QR code. The QR payload contains the `registrationId`.

**Request body:**

```json
{
  "registrationId": 101,
  "isManuallyCheckedIn": false
}
```

**Flow:**

```
1. Look up ProgramUserAttendance by registrationId
2. If not found → 400 INVALID_QR_TOKEN
3. If isAttended = true → return existing record (idempotent, no re-write)
4. Set isAttended = true
        checkedInAt = now()
        checkedInByUserId = req.user.id
        isManuallyCheckedIn = (from request, default false)
5. Save and return updated record
```

**Response (200):**

```json
{
  "success": true,
  "message": "Check-in successful",
  "data": {
    "id": 55,
    "isAttended": true,
    "checkedInAt": "2026-05-21T09:15:00.000Z",
    "checkedInByUserId": 8319
  }
}
```

> If the registrant was **already checked in**, the response message changes to `"Already attended"` and the existing record is returned unchanged.

---

### 4.2 Manual Check-In

**`POST /qr-attendance/manual-checkin`**

Marks a registrant as attended without requiring a QR scan. Useful when a registrant's phone is unavailable or QR cannot be scanned.

**Request body:**

```json
{
  "registrationId": 101,
  "sessionId": 5,
  "notes": "VIP guest"
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `registrationId` | Yes | Must already have a QR (attendance record must exist) |
| `sessionId` | No | Overrides the session stored on the attendance record |
| `notes` | No | Free-text reason for manual check-in |

**Difference from scan:** always sets `isManuallyCheckedIn = true`. Throws `404` if no attendance record exists — the QR must be generated first.

---

### 4.3 Undo Check-In

**`POST /qr-attendance/undo-checkin`**

Reverts one or more check-ins. All records are processed in a single transaction.

**Request body:**

```json
{
  "attendanceIds": [55, 56, 57]
}
```

**What gets reset per record:**

| Field | Reset to |
|-------|----------|
| `isAttended` | `false` |
| `isManuallyCheckedIn` | `false` |
| `checkedInAt` | `null` |
| `checkedInByUserId` | `null` |

Throws `404` if any `attendanceId` in the array does not exist.

---

## 5. List & Download

**`GET /qr-attendance`**

**Query parameters:**

| Param | Type | Description |
|-------|------|-------------|
| `programId` | `number` | Filter by program |
| `sessionId` | `number` | Filter by session |
| `isAttended` | `boolean` | `true` = checked in only · `false` = yet to check in |
| `checkedInByUserId` | `number` | Filter by the admin who performed check-in |
| `search` | `string` | Case-insensitive search on `fullName`, `email`, `registrationSeqNumber` |
| `limit` | `number` | Page size (default: `20`) |
| `offset` | `number` | Page offset (default: `0`) |
| `isDownload` | `boolean` | `true` → generates Excel and returns S3 URL |

**Response (200):**

```json
{
  "success": true,
  "data": {
    "data": [ ...AttendanceResponseDto ],
    "total": 42,
    "limit": 20,
    "offset": 0,
    "statusCounts": [
      { "status": "All",             "count": 42 },
      { "status": "Checked In",      "count": 18 },
      { "status": "Yet to Check In", "count": 24 }
    ]
  }
}
```

**Download response (200):**

```json
{
  "success": true,
  "data": {
    "fileUrl": "https://s3.../attendance/20260521143000/CheckInData_20260521143000.xlsx"
  }
}
```

**Excel columns:** S.No · Seq Number · Full Name · Email · Mobile · Gender · Date of Birth · City · Country · Program Name · Session Name · Session Start · Session End · Registration Date · Registration Status · Check-in Status · Manual Check-in · Checked In At · Checked In By · RM Name · QR URL

---

## 6. Data Model

### `program_user_attendance`

| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| `id` | `int` | No | Primary key |
| `registration_id` | `bigint` | Yes | FK → `hdb_program_registration.id` |
| `user_id` | `int` | Yes | FK → `users.id` |
| `program_id` | `int` | Yes | Denormalised for fast filtering |
| `session_id` | `int` | Yes | FK → `program_session.id` |
| `registration_seq_number` | `varchar(50)` | Yes | Denormalised for search |
| `full_name` | `varchar(255)` | Yes | Snapshot at QR generation time |
| `email` | `varchar(255)` | Yes | Snapshot at QR generation time |
| `mobile` | `varchar(50)` | Yes | Snapshot at QR generation time |
| `rm_name` | `varchar(255)` | Yes | Snapshot at QR generation time |
| `qr_url` | `text` | Yes | S3 URL of the QR JPEG |
| `is_attended` | `boolean` | No | `false` until first check-in |
| `is_manually_checked_in` | `boolean` | No | `true` if checked in without QR scan |
| `checked_in_at` | `timestamptz` | Yes | Timestamp of first check-in |
| `checked_in_by_user_id` | `int` | Yes | FK → `users.id` (admin who checked in) |
| `created_at` | `timestamptz` | No | Auto-set on insert |
| `updated_at` | `timestamptz` | No | Auto-updated on every save |
| `created_by` | `int` | Yes | Admin who generated the QR |
| `updated_by` | `int` | Yes | Last admin to update the record |

### `background_jobs`

Tracks async bulk QR generation jobs.

| Column | Type | Description |
|--------|------|-------------|
| `id` | `bigint` | Primary key |
| `type` | `enum` | `BULK_QR_GENERATION` |
| `status` | `enum` | `pending` / `processing` / `completed` / `failed` |
| `total` | `int` | Total registrations to process |
| `generated` | `int` | Successfully generated count |
| `skipped` | `int` | Skipped (already had QR or ineligible) |
| `failed` | `int` | Failed count |
| `program_id` | `bigint` | Program the job is for |
| `folder_name` | `varchar` | S3 folder for this batch |
| `s3_url` | `text` | Base S3 URL |
| `metadata` | `jsonb` | `{ sessionId, batchSize }` |
| `completed_at` | `timestamptz` | Set when job finishes |

---

## 7. Error Codes

| Code | HTTP | Trigger |
|------|------|---------|
| `PROGRAM_ATTENDANCE_REGISTRATION_NOTFOUND` | 404 | `registrationId` not found in `hdb_program_registration` |
| `PROGRAM_ATTENDANCE_REGISTRATION_NOT_ELIGIBLE` | 400 | Registration fails eligibility check |
| `PROGRAM_ATTENDANCE_INVALID_QR_TOKEN` | 400 | No attendance record found for the scanned `registrationId` |
| `PROGRAM_ATTENDANCE_NOTFOUND` | 404 | Attendance record ID not found |
| `PROGRAM_ATTENDANCE_BULK_JOB_NOTFOUND` | 404 | Bulk job ID not found |
| `PROGRAM_ATTENDANCE_QR_TOKEN_GENERATION_FAILED` | 500 | S3 upload or QR image generation error |
| `PROGRAM_ATTENDANCE_BULK_GENERATION_FAILED` | 500 | Bulk job startup error |
| `PROGRAM_ATTENDANCE_SAVE_FAILED` | 500 | DB write error |
| `PROGRAM_ATTENDANCE_GET_FAILED` | 500 | DB read error |

---

## 8. Key Files

| File | Purpose |
|------|---------|
| `src/qr-attendance/qr-attendance.controller.ts` | HTTP layer — guards, request shaping, response formatting |
| `src/qr-attendance/qr-attendance.service.ts` | Business logic — eligibility, QR generation, transactions |
| `src/qr-attendance/qr-attendance.repository.ts` | DB queries — selective field loading, optimised joins |
| `src/qr-attendance/qr-attendance.constants.ts` | Shared constants — excluded statuses, paid statuses, select-field arrays |
| `src/qr-attendance/dto/generate-qr.dto.ts` | Input DTO for single QR generation |
| `src/qr-attendance/dto/generate-bulk.dto.ts` | Input DTO for bulk QR generation |
| `src/qr-attendance/dto/scan-qr.dto.ts` | Input DTO for QR scan / check-in |
| `src/qr-attendance/dto/manual-checkin.dto.ts` | Input DTO for manual check-in |
| `src/qr-attendance/dto/undo-checkin.dto.ts` | Input DTO for undo check-in |
| `src/qr-attendance/dto/get-attendance-list.dto.ts` | Query params DTO for list + download |
| `src/qr-attendance/dto/attendance-response.dto.ts` | Response shape — maps entity → API response |
| `src/common/entities/background-job.entity.ts` | `background_jobs` table entity |

---

> **Note on session joins:** The session relation uses `leftJoinAndMapOne` with an explicit `ON s.id = pa.session_id` condition instead of following the TypeORM relation. This bypasses the automatic `deleted_at IS NULL` soft-delete filter so sessions remain visible in attendance records even after being soft-deleted.
