# HR Expo 2026 API

Stripped-down, **single-event** version ng DTI/Toast events API for HR Expo 2026. Tinanggal na
ang lahat ng multi-event, products, customers, messages, atbp. — ito na lang
ang natira:

1. **Register** (public, dynamic fields — `registration_fields` table; `source` is `online`, `assisted`, or `kiosk`; duplicate email addresses are allowed, while trim + lowercase normalization is used for lookups)
2. Pag-register → **email QR code** (HREX-branded, subject: `HR EXPO 2026: Your Registration QR Code`)
3. Staff **scans that QR sa counter** → **badge is "printed"**: name, company, contact + **BAGONG QR** (link-type, e.g. `.../profile?p=REFERENCE`)
4. Yung bagong QR sa badge ang **sinasscan sa entrance** → nire-reuse ang existing attendance, o nire-record ito kung wala pa.

## Setup (local)

```bash
cp .env.example .env
# i-edit ang .env: DB creds, SECRETKEY, mail creds, BADGE_QR_BASE_URL

npm install

# gumawa ng empty MySQL db na tugma sa DB_DATABASE sa .env, tapos:
npm run dev
# app.js runs connection.sync() by default sa dev — awtomatikong gagawin
# ang mga tables. Kung gusto niyo plain SQL, gamitin na lang yung sql/schema.sql
# (may kasama pang current static registration fields).

# Existing databases: apply the non-destructive upgrades before using the HREX
# form, long consent labels, or normalized email lookup/resend behavior:
# sql/migrations/20260824_registration_labels_and_active_email.sql
# sql/migrations/20260907_hrex_2026_registration_form.sql

# gumawa ng unang staff/scanner account gamit ang strong password:
node scripts/seed-staff.js <username> <strong-password> admin "Admin User"
```

Server runs sa `http://localhost:4000` by default (`PORT` sa `.env`).

### Final isolated full-path verification

For deployment-readiness verification, run:

```bash
npm run verify:full-path
```

This creates a uniquely named temporary MySQL database, imports
`sql/schema.sql`, starts the real Express app on a temporary localhost port,
exercises the HTTP workflow, and drops only that temporary database after a
successful run. It loads the local `.env` for MySQL and real SMTP delivery but
does not edit it. The verification requires a reachable MySQL account with
permission to create/drop temporary databases and working local SMTP values in
`.env`. It is intentionally separate from `npm test` and should not be run by
ordinary CI unless those local services and credentials are available.

## Flow / Endpoints

| Step | Endpoint | Auth |
|---|---|---|
| Get dynamic form fields (for the register page) | `GET /v1/registration-fields` | public |
| Register | `POST /v1/registrations/register` | public |
| **Preview/execute Excel preregistration import** | `POST /v1/registrations/import-excel` | admin |
| **Resend registration QR email (recovery)** | `POST /v1/registrations/resend-email` | public (IP rate-limited 5/15min + per-email cooldown 10min) |
| **Resend registration QR email (recovery)** | `POST /v1/registrations/:guid/resend-email` | deployed compatibility route (currently no auth middleware; security-sensitive) |
| Staff login | `POST /v1/staff-auth/login` | public |
| **Public Profile** — phone scans printed badge QR → business-card page | `GET /v1/registrations/profile/:referenceNumber` | public (rate-limited, read-only) |
| Online badge issuance — scan Registration QR → issue/print badge | `GET /v1/registrations/scan/registration/:hashedId` | badge_printer or admin |
| Assisted/kiosk badge issuance — issue/print badge directly | `POST /v1/registrations/:guid/issue-badge` | badge_printer or admin |
| Entrance — scan badge QR → record attendance | `GET /v1/registrations/scan/entrance/:referenceNumber` | scanner or admin |

Buong listahan ng endpoints (CRUD ng fields, listing, stats, atbp.) nasa
Postman collection: `postman/HREX2026.postman_collection.json` —
i-import sa Postman, i-set lang ang `baseUrl` variable
(default `http://localhost:4000`), tapos i-run muna yung **Staff Login**
request para awtomatikong ma-set yung `staffToken` variable ng ibang requests.

### Paano gumagana yung dalawang QR

- **QR #1 (registration QR)** — ginagawa pag nag-`register`, ito yung
  ini-email. Para sa `source: "online"`, ibinabalik din ng registration
  response ang parehong generated QR image (`data.registrationQr`), kaya
  walang ikalawang RegistrationQR identity. Naka-encode dito ay isang hashed
  token (`hashedId`), hindi yung raw guid. Pag na-scan ito ng counter staff
  (`GET /scan/registration/:hashedId`), doon gagawin/i-return yung data para
  sa **badge**: fullName, company, contact, at **QR #2**; dito rin unang
  nire-record ang attendance.
- **QR #2 (badge QR)** — link-type QR (hal.
  `https://yourdomain.com/profile?p=HREX-7F3K9Q`), naka-print mismo sa
  badge plate. Ito yung sinasscan sa **entrance**
  (`GET /scan/entrance/:referenceNumber` — `referenceNumber` yung
  `p=` value na kinuha mula sa scanned URL). Nire-reuse nito ang
  `dateAttended` na na-record sa badge printing, at nagre-record lang kung wala pa.

For `source: "assisted"` and `source: "kiosk"`, registration and badge
issuance are separate requests at the venue. The badge-printer calls
`POST /v1/registrations/:guid/issue-badge`; it uses the same badge issuance
  logic as the online Registration QR scan, reuses an existing BadgeQR, and
  records attendance on first issue. Direct issuance rejects `source: "online"`.

### Staff roles

Ang `scanner_users.role` ang nag-e-enforce kung sino ang pwedeng mag-operate
ng bawat scanning step (enforced sa `middlewares/requireRole.js`):

| Role | Pwede gawin |
|---|---|
| `badge_printer` | Online Registration QR scan or assisted/kiosk direct badge issuance; badge printing records first attendance. Hindi pwede mag-Entrance scan. |
| `scanner` | Entrance scan lang (mag-record ng `dateAttended`/`scanned_by` kung wala pa). Hindi pwede mag-print ng badge. |
| `admin` | All-around operational role: badge issuance/printing, Entrance scan, at management/listing/CRUD. |

> Ang `admin` ay pinapayagan sa parehong badge issuance paths at sa Entrance
> scan. Ang `scanner` ay hindi maaaring mag-print ng badge, at ang
> `badge_printer` ay hindi maaaring mag-Entrance scan.

### Excel preregistration import

The admin-only `POST /v1/registrations/import-excel` endpoint accepts one
memory-backed `.xlsx` upload in the `file` multipart field. It also accepts
`dryRun` (default `true`), `sendEmail` (default `true`), `source` (default
`online`), and optional JSON fields `columnMappings`, `ignoredColumns`, and
`defaults`. Use **Preview Excel Import** in the latest Postman collection first;
the preview validates the current active `registration_fields`, required fields,
radio/checkbox options, and unknown columns without writing registrations,
answers, QR rows, email logs, or sending mail. Duplicate email addresses are
allowed both against the database and within the workbook. Execute only after
`readyForExecution` is `true`. Each executable row is isolated, so an invalid
row, SMTP failure, or other row-level failure does not abort the rest of the
batch. `sendEmail=false` still creates the normal registration,
answer, and registration QR records but deliberately sends no mail and creates
no email log.

The importer supports deterministic field-key/label matching plus narrowly
scoped Microsoft Forms aliases. Unknown columns are never silently discarded;
map them with `columnMappings` or explicitly list them in `ignoredColumns`.
Only active `registration_fields.fieldKey` values can be mapping targets. The
upload is limited to 10 MB and 10,000 data rows. Legacy `.xls` files are not
accepted; save them as `.xlsx` before importing.

### Registration QR email resend (recovery)

Kapag na-save ang registration at ang `registration_qr`, pero **bigo ang SMTP
delivery** (e.g. nag-expire ang app password, offline ang Gmail), **registered
pa rin ang attendee**. Para makuha ulit niya ang EXISTING QR email:

- **Public self-service** — `POST /v1/registrations/resend-email`
  ```json
  { "email": "attendee@example.com" }
  ```
  - Reuses the EXISTING registrant + `registration_qr.hashedId` — **hindi**
    gumagawa ng panibagong registration, QR identity, o hashedId.
  - **Generic response** palagi (`"If a registration exists for that email, a
    QR email will be sent."`) — hindi nito inilalabas kung registered ang email
    o kung successful ang send (anti-enumeration). Ang totoong outcome ay nasa
    `email_logs` lang.
  - Protected ng **IP rate limit** (5 req / 15 min) at **per-email cooldown**
    (1 send / 10 min, backed by the existing `email_logs` table — walang
    bagong table/migration).
- **Admin manual resend** — `POST /v1/registrations/:guid/resend-email`
  (deployed compatibility behavior; currently no auth middleware — security-sensitive)
  - Same reuse rule (existing registrant + existing `hashedId`), pero
    **truthful** ang response (`data.emailDelivered`). Sa SMTP failure,
    **registered pa rin** ang attendee at walang raw SMTP error na lumalabas.

Walang side-effect sa attendance/badge: ang resend ay hindi Self Check-in, hindi
badge print, at hindi entrance scan — hindi nito ginagalaw ang `badge_qr`,
`dateAttended`, `scanned_by`, `printedAt`, o `printed_by`.

Maaaring i-verify ang buong flow (real Gmail + local DB, walang babaguhin sa
`.env`) gamit ang:
```bash
node scripts/verify-resend-flow.js
```

Gumawa ng accounts gamit ang `scripts/seed-staff.js`, e.g.:
```bash
node scripts/seed-staff.js admin <password> admin "Admin User"
node scripts/seed-staff.js badge_printer <password> badge_printer "Counter 1"
node scripts/seed-staff.js scanner <password> scanner "Entrance Scanner 1"
```

### Dynamic fields / badge roles

The active HREX form includes a required multi-select **Purpose of Visit** with
these seven options:

- Discover new HR products and solutions
- Find HR technology, AI, and automation solutions
- Explore recruitment, talent, and employee development solutions
- Find employee benefits, wellness, and engagement solutions
- Learn about the latest HR trends and best practices
- Attend/ Listen to talks
- Others

It also includes the required `data_privacy_consent` checkbox. Its only
accepted value is `yes`, and its full label is stored in the database so the
complete AFT Corp/Data Privacy Act of 2012 statement is shown and retained.

`registration_fields.label` is stored as MySQL `TEXT`, so long-form consent
and question text is returned unchanged by `GET /v1/registration-fields` and
is also preserved in the readable registration-answer copy. The migration
widens the existing columns without truncating labels. The migration also adds
the nullable normalized email lookup key; duplicate active addresses remain
allowed, so no active-email unique key or duplicate preflight is required.

Sa `registration_fields`, ang "system" roles na kailangan para
gumana ang buong flow:

- `fullName` — computed mula sa required `firstName`/`lastName`, ginagamit sa email at sa badge
- `email` — required, dito ipapadala ang QR
- `mobileNumber` — para sa "Contact" sa badge
- `company` — para sa "Company" sa badge
- `position`, `purpose_of_visit`, and `data_privacy_consent` — canonical values remain dynamic-only in
  the `responses` JSON and `registration_field_answers` rows; they are not
  mirrored into convenience columns on `registrants`

### HR Expo 2026 event and email configuration

- Event: **HR Expo 2026**
- Dates: **September 22, 2026 (Tuesday) – September 23, 2026 (Wednesday)**
- Email sender name: **HR Expo**
- Registration QR subject: **HR EXPO 2026: Your Registration QR Code**
- Registration QR support contact: `operations@aftcorpph.com`
- The same email QR is valid for one registration/check-in use. A visitor
  attending the other event day should submit another registration; duplicate
  email addresses are intentionally accepted and create separate QR identities.

Lahat ng ibang fields (T-shirt size, industry, atbp.) ay puro dagdag na
data lang — nasa `responses` JSON column at sa
`registration_field_answers` (para sa readable admin grid).

## Not included (sinadya, in-strip out)

- Multi-event / event listing / event CRUD
- Products, customers, messages, inquiries, reviews, announcements, atbp.
- Attendance "thank you" email (di na kasama sa binanggit na flow — puwede
  idagdag later sa `controllers/registration.controller.js#scanEntrance`
  kung gusto niyo)
