npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mostajs/ticketing

v1.0.3

Published

Reusable ticketing module — ticket lifecycle, scan validation, quota management, API route factories

Readme

@mostajs/ticketing

Reusable ticketing module — ticket lifecycle, scan validation, quota management, API route factories.

npm version license

Part of the @mosta suite. Depends only on @mostajs/orm. Zero coupling with auth, RBAC, or any framework beyond standard Request/Response.


Table des matieres

  1. Installation
  2. Concepts cles
  3. Quick Start (5 etapes)
  4. Formats de codes supportes
  5. API Route Factories
  6. Core Logic (fonctions pures)
  7. Schemas & Repositories
  8. Integration complete dans une nouvelle app
  9. API Reference
  10. Architecture

Installation

npm install @mostajs/ticketing

Prerequis : @mostajs/orm doit etre configure avec votre base de donnees.


Concepts cles

Cycle de vie d'un ticket

[create] → active → [scan] → used → (expired)
                 ↑           ↓
                 └── day_reentry (re-scan meme jour)

Modes de validite

| Mode | Comportement | |------|-------------| | single_use | Un seul scan, puis used | | day_reentry | Re-entree illimitee le meme jour, quota decremente une seule fois | | time_slot | Valide pendant N minutes apres creation | | unlimited | Pas d'expiration |

Quota

  • totalQuota : nombre total de tickets autorise pour un acces
  • remainingQuota : decremente a chaque scan (sauf re-entrees day_reentry)
  • Status depleted automatique quand quota atteint 0

Formats de codes

Le champ codeFormat permet de generer et scanner differents types de codes :

| Format | Type | Usage typique | |--------|------|--------------| | qrcode | 2D | Le plus courant, haute capacite | | code128 | 1D | Alphanumerique, logistique | | code39 | 1D | Alphanumerique, industrie | | ean13 | 1D | 13 chiffres, retail Europe | | ean8 | 1D | 8 chiffres, petits produits | | upc_a | 1D | 12 chiffres, retail USA | | itf | 1D | Paires numeriques, colis | | pdf417 | 2D | Haute capacite, cartes d'identite | | datamatrix | 2D | Petit format, composants | | aztec | 2D | Compact, cartes d'embarquement |


Quick Start

Etape 1 — Enregistrer les schemas

// src/dal/registry.ts
import { registerSchema } from '@mostajs/orm'
import {
  TicketSchema,
  ClientAccessSchema,
  ScanLogSchema,
  ActivitySchema,
  SubscriptionPlanSchema,
  CounterSchema,
} from '@mostajs/ticketing'

registerSchema(TicketSchema)
registerSchema(ClientAccessSchema)
registerSchema(ScanLogSchema)
registerSchema(ActivitySchema)
registerSchema(SubscriptionPlanSchema)
registerSchema(CounterSchema)

Etape 2 — Instancier les repositories

// src/dal/service.ts
import { getDialect } from '@mostajs/orm'
import {
  TicketRepository,
  ClientAccessRepository,
  ScanLogRepository,
  ActivityRepository,
  SubscriptionPlanRepository,
} from '@mostajs/ticketing'

export async function ticketRepo() {
  return new TicketRepository(await getDialect())
}
export async function clientAccessRepo() {
  return new ClientAccessRepository(await getDialect())
}
export async function scanLogRepo() {
  return new ScanLogRepository(await getDialect())
}
export async function activityRepo() {
  return new ActivityRepository(await getDialect())
}
export async function planRepo() {
  return new SubscriptionPlanRepository(await getDialect())
}

Etape 3 — Route scan (POST /api/scan)

// src/app/api/scan/route.ts
import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
import { ticketRepo, clientAccessRepo, scanLogRepo, clientRepo } from '@/dal/service'
import { checkPermission } from '@/lib/authCheck'

export const { POST } = createScanHandler({
  checkAuth: async () => {
    const { error, session } = await checkPermission('scan:validate')
    return { error: error || null, userId: session?.user?.id || '' }
  },

  getRepositories: async () => ({
    ticketRepo: await ticketRepo(),
    clientAccessRepo: await clientAccessRepo(),
    scanLogRepo: await scanLogRepo(),
    clientRepo: await clientRepo(),
  }),

  // Optionnel : audit apres scan reussi
  onGranted: async ({ ticket, client, isReentry, userId }) => {
    console.log(`Scan ${isReentry ? 'reentry' : 'granted'}: ${ticket.ticketNumber}`)
  },
})

Etape 4 — Route tickets (GET + POST /api/tickets)

// src/app/api/tickets/route.ts
import { createTicketsHandler } from '@mostajs/ticketing/api/tickets.route'
import { ticketRepo, clientRepo, clientAccessRepo, activityRepo } from '@/dal/service'
import { checkPermission } from '@/lib/authCheck'

export const { GET, POST } = createTicketsHandler({
  checkAuth: async (req, permission) => {
    const { error, session } = await checkPermission(permission)
    return { error: error || null, userId: session?.user?.id || '' }
  },

  getRepositories: async () => ({
    ticketRepo: await ticketRepo(),
    clientRepo: await clientRepo(),
    clientAccessRepo: await clientAccessRepo(),
    activityRepo: await activityRepo(),
  }),

  // Format de code par defaut pour les nouveaux tickets
  defaultCodeFormat: 'qrcode',

  // Optionnel : callback apres creation
  onCreated: async ({ ticket, userId }) => {
    console.log(`Ticket ${ticket.ticketNumber} cree`)
  },
})

Etape 5 — Tester

# Creer un ticket
curl -X POST http://localhost:3000/api/tickets \
  -H 'Content-Type: application/json' \
  -d '{"clientId": "abc", "activityId": "xyz"}'

# Scanner un ticket
curl -X POST http://localhost:3000/api/scan \
  -H 'Content-Type: application/json' \
  -d '{"code": "uuid-du-ticket"}'

# Avec un code-barres specifique
curl -X POST http://localhost:3000/api/tickets \
  -H 'Content-Type: application/json' \
  -d '{"clientId": "abc", "activityId": "xyz", "codeFormat": "code128"}'

API Route Factories

createScanHandler(config)

| Option | Type | Description | |--------|------|-------------| | getRepositories | () => Promise<{...}> | Fournit ticketRepo, clientAccessRepo, scanLogRepo, clientRepo | | checkAuth | (req) => Promise<{error, userId}> | Verifie auth + permissions | | onGranted? | (data) => Promise<void> | Callback apres scan reussi (audit, notifications) | | onDenied? | (data) => Promise<void> | Callback apres scan refuse (alertes) |

Requete :

POST /api/scan
{ "code": "uuid-or-barcode-value", "scanMethod": "webcam" }

Reponse (granted) :

{
  "data": {
    "result": "granted",
    "isReentry": false,
    "ticket": { "ticketNumber": "TKT-20260306-0001", "clientName": "Alice Dupont", ... },
    "client": { "name": "Alice Dupont", "photo": "/photos/alice.jpg" },
    "access": { "remainingQuota": 9, "totalQuota": 10, "status": "active" }
  }
}

Reponse (denied) :

{
  "data": {
    "result": "denied",
    "reason": "ticket_already_used",
    "ticket": { "ticketNumber": "TKT-20260306-0001", ... }
  }
}

createTicketsHandler(config)

| Option | Type | Description | |--------|------|-------------| | getRepositories | () => Promise<{...}> | Fournit ticketRepo, clientRepo, clientAccessRepo, activityRepo | | checkAuth | (req, permission) => Promise<{error, userId}> | Auth avec nom de permission (ticket:view, ticket:create) | | onCreated? | (data) => Promise<void> | Callback apres creation ticket | | defaultCodeFormat? | CodeFormat | Format de code par defaut (defaut: 'qrcode') |


Core Logic

Fonctions pures, utilisables partout (serveur, worker, CLI, tests) :

processScan(code, scanMethod, scannedBy, deps)

Pipeline 8 etapes de validation. Toute l'I/O est injectee via deps.

import { processScan } from '@mostajs/ticketing'
import type { ScanDeps } from '@mostajs/ticketing'

const deps: ScanDeps = {
  findTicketByCode: async (code) => db.tickets.findOne({ code }),
  findAccessById: async (id) => db.accesses.findById(id),
  findClientById: async (id) => db.clients.findById(id),
  wasScannedToday: async (ticketId) => { /* ... */ },
  updateTicket: async (id, data) => db.tickets.update(id, data),
  updateAccess: async (id, data) => db.accesses.update(id, data),
  createScanLog: async (data) => db.scanLogs.create(data),
  resolveId: (ref) => typeof ref === 'string' ? ref : ref.id,
  formatClientName: (c) => `${c.firstName} ${c.lastName}`,
}

const result = await processScan('ticket-uuid', 'webcam', 'user-123', deps)

computeValidUntil(mode, durationMinutes)

import { computeValidUntil } from '@mostajs/ticketing'

computeValidUntil('day_reentry', null)  // → fin de journee (23:59:59)
computeValidUntil('time_slot', 90)      // → now + 90 minutes
computeValidUntil('single_use', null)   // → null

decrementQuota(remainingQuota)

import { decrementQuota } from '@mostajs/ticketing'

decrementQuota(5)    // → { remainingQuota: 4 }
decrementQuota(1)    // → { remainingQuota: 0, status: 'depleted' }
decrementQuota(null) // → null (unlimited)

Schemas & Repositories

Schemas disponibles

| Schema | Collection | Description | |--------|-----------|-------------| | TicketSchema | tickets | Ticket avec code (QR/barcode), validite, statut | | ClientAccessSchema | client_accesses | Acces client-activite avec quota | | ScanLogSchema | scan_logs | Journal des scans (granted/denied) | | ActivitySchema | activities | Activites avec planning et mode de validite | | SubscriptionPlanSchema | subscription_plans | Plans d'abonnement | | CounterSchema | counters | Auto-increment interne |

Repositories

| Repository | Methodes cles | |-----------|--------------| | TicketRepository | createWithAutoFields(), findByCode(), markUsed(), countByAccess() | | ClientAccessRepository | findActiveAccess(), decrementQuota(), block() | | ScanLogRepository | wasScannedToday(), countToday(), findDistinctClientsToday() | | ActivityRepository | findActive(), findBySlug(), findAllOrdered() | | SubscriptionPlanRepository | findActive(), findAllWithActivities() |


Integration complete

Nouvelle app Next.js depuis zero

# 1. Creer le projet
npx create-next-app@latest my-ticketing-app
cd my-ticketing-app

# 2. Installer
npm install @mostajs/orm @mostajs/ticketing

# 3. Configurer la DB
echo 'DATABASE_URL=mongodb://localhost:27017/myapp' >> .env.local

# 4. Enregistrer les schemas dans src/dal/registry.ts
# 5. Creer les repo helpers dans src/dal/service.ts
# 6. Creer les routes API (2 fichiers, ~30 lignes chacun)
# 7. npm run dev

Avec audit (@mostajs/audit)

import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
import { logAudit, getAuditUser } from '@mostajs/audit/lib/audit'

export const { POST } = createScanHandler({
  // ...
  onGranted: async ({ ticket, client, isReentry, userId }) => {
    await logAudit({
      userId,
      action: isReentry ? 'scan_reentry' : 'scan_granted',
      module: 'scan',
      resource: ticket.ticketNumber,
    })
  },
  onDenied: async ({ reason, ticket, userId }) => {
    await logAudit({
      userId,
      action: 'scan_denied',
      module: 'scan',
      resource: ticket?.ticketNumber,
      details: { reason },
    })
  },
})

Avec un scanner de code-barres physique

// Le scanner physique envoie le meme POST /api/scan
// Seul le scanMethod change
fetch('/api/scan', {
  method: 'POST',
  body: JSON.stringify({
    code: 'TKT-20260306-0001',  // valeur lue par le scanner
    scanMethod: 'handheld_scanner',
  }),
})

API Reference

Types

| Type | Description | |------|-------------| | CodeFormat | 'qrcode' \| 'code128' \| 'code39' \| 'ean13' \| 'ean8' \| 'upc_a' \| 'itf' \| 'pdf417' \| 'datamatrix' \| 'aztec' | | ValidityMode | 'day_reentry' \| 'single_use' \| 'time_slot' \| 'unlimited' | | TicketStatus | 'active' \| 'used' \| 'expired' \| 'cancelled' | | ScanResult | 'granted' \| 'denied' | | ScanMethod | 'webcam' \| 'pwa_camera' \| 'handheld_scanner' \| 'nfc' | | AccessType | 'unlimited' \| 'count' \| 'temporal' \| 'mixed' | | DenyReason | 'invalid_ticket' \| 'ticket_already_used' \| 'ticket_expired' \| 'ticket_cancelled' \| 'quota_depleted' \| 'access_expired' \| 'client_suspended' | | ScanDeps | Interface d'injection pour processScan() | | ScanHandlerConfig | Config de createScanHandler() | | TicketsHandlerConfig | Config de createTicketsHandler() |


Architecture

@mostajs/ticketing
├── schemas/
│   ├── ticket.schema.ts          # Ticket (code multi-format, validite, statut)
│   ├── client-access.schema.ts   # Acces client-activite avec quota
│   ├── scan-log.schema.ts        # Journal des scans
│   ├── activity.schema.ts        # Activites (planning, mode validite)
│   ├── subscription-plan.schema.ts # Plans d'abonnement
│   └── counter.schema.ts         # Auto-increment sequences
├── repositories/
│   ├── ticket.repository.ts      # CRUD + findByCode, createWithAutoFields
│   ├── client-access.repository.ts # Quota, findActiveAccess
│   ├── scan-log.repository.ts    # wasScannedToday, countToday
│   ├── activity.repository.ts    # findActive, findBySlug
│   └── subscription-plan.repository.ts
├── lib/
│   ├── scan-processor.ts         # Pipeline 8 etapes (pure, injectable)
│   ├── validity-checker.ts       # computeValidUntil, isExpired
│   └── quota-manager.ts          # decrementQuota, wouldExceedQuota
├── api/
│   ├── scan.route.ts             # Factory createScanHandler(config)
│   └── tickets.route.ts          # Factory createTicketsHandler(config)
├── types/
│   └── index.ts                  # CodeFormat, ValidityMode, ScanDeps, etc.
└── index.ts                      # Barrel exports

Dependances:
  @mostajs/orm   (seule dep runtime)
  next >= 14     (peer, optionnel)

Zero dependance sur: @mostajs/auth, @mostajs/rbac, @mostajs/audit
(l'app injecte ses propres callbacks auth/audit)

Pattern Factory (injection de dependances)

┌──────────────────────┐     inject callbacks      ┌──────────────────────┐
│  @mostajs/ticketing   │ ◄──────────────────────── │   Votre app          │
│                       │                            │                      │
│ createScanHandler({   │                            │ checkAuth: () =>     │
│   checkAuth,          │                            │   verifyToken(...)   │
│   getRepositories,    │                            │ getRepositories: ()  │
│   onGranted,          │                            │   => { ticketRepo,   │
│ })                    │                            │     scanLogRepo }    │
└──────────────────────┘                            └──────────────────────┘

License

MIT — Dr Hamid MADANI [email protected]