# @classytic/muster

> Universal attendance primitive. One engine for corporate HR clock-in, gym turnstiles, university classrooms, doctor appointments, warehouse gates, and any other "was X present at Y during Z" use-case.

**Subject-polymorphic.** `@classytic/muster` has zero opinions about who is being tracked. ERPNext HR and Odoo HR hardcode `Employee`; muster uses `subjectRef: string` + `subjectModel: string` (SourceBridge pattern). The host maps sessions to any entity — `Employee`, `Patient`, `Student`, `GymMember`, `Visitor`.

**Framework-agnostic.** MongoKit repositories are the domain layer. Arc auto-CRUD via `defineResource()` with zero adapter code. Hosts without Arc can call repositories directly.

## Install

```bash
npm install @classytic/muster @classytic/mongokit @classytic/primitives mongoose zod
```

## Core shape

```
AttendanceSession ──────┬── AttendanceRecord (append-only event log)
  subjectRef             │     check_in / check_out / break_start /
  subjectModel           │     break_end / correction_applied
  scope                  │
  scheduledStart/End     │
  actualStart/End        │
  status: open|closed|   │
          cancelled      │
  outcome: present|late| │
           absent|...    │
  checkInMethod          │
  checkOutMethod         │
                         │
  AttendanceCorrection ──┘
    proposedChanges
    status: pending|
            approved|rejected
    reviewedBy
```

## Quick start

```typescript
import mongoose from 'mongoose';
import { createMuster } from '@classytic/muster';

const muster = await createMuster({
  connection: mongoose.connection,
  tenant: { fieldType: 'objectId' }, // repo-core TenantConfig; this is the default
});

// Corporate HR: employee clocking in via kiosk
const session = await muster.repositories.attendanceSession.checkIn(
  {
    subjectRef: String(employee._id),
    subjectModel: 'Employee',
    scope: String(branch._id),
    method: 'kiosk',
    scheduledStart: new Date('2026-04-18T09:00:00Z'),
    scheduledEnd:   new Date('2026-04-18T17:00:00Z'),
  },
  ctx,
);

// Gym access: member tapping an RFID card
await muster.repositories.attendanceSession.checkIn(
  {
    subjectRef: member.memberNumber,
    subjectModel: 'GymMember',
    scope: 'downtown-gym',
    method: 'rfid',
    device: { deviceId: 'turnstile-3', deviceType: 'turnstile' },
  },
  ctx,
);

// University class: student scanning a QR code
await muster.repositories.attendanceSession.checkIn(
  {
    subjectRef: String(student._id),
    subjectModel: 'Student',
    scope: 'classroom-101',
    method: 'qr',
    scheduledStart: new Date('2026-04-18T10:00:00Z'),
    scheduledEnd:   new Date('2026-04-18T11:30:00Z'),
  },
  ctx,
);

// Check out
await muster.repositories.attendanceSession.checkOut(
  String(session._id),
  { method: 'kiosk' },
  ctx,
);
```

> **Transactions required.** `checkIn` / `checkOut` / breaks / `cancel` / `correction.approve` are multi-write transactional verbs — `createMuster()` asserts `repo.capabilities.transactions` at boot and MongoDB must run as a replica set. Local-dev on standalone Mongo opts out with `allowNonTransactional: true` (accepting partial-write risk). For production index management use `autoIndex: false` + `await muster.syncIndexes()` from a deploy script.

## Events

Hosts subscribe via the Arc-compatible `EventTransport`. Any arc transport (Memory, Redis, Kafka) drops in.

```typescript
await muster.events.subscribe('muster:session.*', async (event) => {
  // handle muster:session.opened / .closed / .cancelled / .corrected
});

await muster.events.subscribe('muster:correction.approved', async (event) => {
  // payroll / attendance reports get notified when a correction is applied
});
```

Event names:

| Name                          | Fired on                                               |
| ----------------------------- | ------------------------------------------------------ |
| `muster:session.opened`       | `checkIn()` creates a new session                       |
| `muster:session.closed`       | `checkOut()` closes an open session                     |
| `muster:session.cancelled`    | `cancel()` voids an open session                        |
| `muster:session.corrected`    | A correction is approved and applied                    |
| `muster:record.appended`      | Any `AttendanceRecord` is written                       |
| `muster:correction.requested` | A correction request is filed                           |
| `muster:correction.approved`  | Reviewer approves a correction                          |
| `muster:correction.rejected`  | Reviewer rejects a correction                           |

Every event has a Zod-source definition in the catalog (`@classytic/muster/events`). Hosts using arc register them for publish-time validation and OpenAPI event introspection:

```typescript
import { musterEventDefinitions } from '@classytic/muster/events';

for (const def of musterEventDefinitions) registry.register(def);
```

Timestamps in payloads are ISO-8601 strings on the wire (`new Date(payload.actualStart)` to get a `Date`). All domain errors implement `HttpError` from `@classytic/repo-core/errors` — `status` plus a hierarchical `code` (`muster.session.already_open`, `muster.concurrency.version_mismatch`, …) — so arc serializes them with the right HTTP status automatically.

## Check-in methods

```typescript
type CheckInMethod =
  | 'manual'      // admin entered the time manually
  | 'kiosk'       // on-premise touchscreen
  | 'qr'          // student/patient scanned a QR code
  | 'rfid'        // card/badge tap at a reader
  | 'biometric'   // fingerprint / palm vein
  | 'facial'      // face recognition camera
  | 'mobile'      // in-app check-in
  | 'geofence';   // auto check-in from GPS fence
```

Biometric vendors own the template store. Muster records only the method + an opaque `device.deviceId`.

## Arc integration

```typescript
import { defineResource } from '@classytic/arc';

export const sessionResource = defineResource({
  name: 'attendance-session',
  prefix: '/attendance/sessions',
  adapter: createAdapter(muster.models.AttendanceSession, muster.repositories.attendanceSession),
  actions: {
    checkIn:  { handler: (_id, data, req) => muster.repositories.attendanceSession.checkIn(data, req.scope) },
    checkOut: { handler: (id, data, req) => muster.repositories.attendanceSession.checkOut(id, data, req.scope) },
    cancel:   { handler: (id, data, req) => muster.repositories.attendanceSession.cancel(id, data, req.scope) },
  },
});
```

CRUD, pagination, filtering, soft-delete query support — all inherited from mongokit + Arc adapter.

## Why "muster"?

Military tradition: a formal gathering of personnel for accountability. The word has no "employee" bias built in.
