@mostajs/allocations 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +29 -0
- package/docs/00-AUDIT-EXTRACTION-ALLOCATIONS-18062026.md +94 -0
- package/docs/03-PLAN-DEV-ALLOCATIONS.md +82 -0
- package/examples/casiers-gare/run.mjs +39 -0
- package/examples/casiers-salle-sport/run.mjs +43 -0
- package/examples/showcase-3-modules/run.mjs +37 -0
- package/examples/velos/run.mjs +42 -0
- package/examples/web-demo/README.md +22 -0
- package/examples/web-demo/app.mjs +75 -0
- package/examples/web-demo/index.html +71 -0
- package/examples/web-demo/serve.sh +13 -0
- package/llms.txt +30 -0
- package/package.json +28 -0
- package/src/allocations.js +93 -0
- package/src/index.js +6 -0
- package/src/memory-repo.js +21 -0
- package/src/profiles.js +15 -0
- package/src/schemas.js +51 -0
- package/test-scripts/unit/allocations.test.mjs +69 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog — @mostajs/allocations
|
|
2
|
+
|
|
3
|
+
Format : Keep a Changelog · SemVer.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] — 2026-06-18
|
|
6
|
+
### Added
|
|
7
|
+
- Schémas `Unit`/`Allocation`/`AllocationEvent` (EntitySchema @mostajs/orm), généralisés du casier SecuAccessPro.
|
|
8
|
+
- `createAllocations()` : `units` (create/list/findAvailable/maintenance/retire), `alloc` (assign/release/renew/markOverdue/findByHolder/history/countByStatus). `guard` d'éligibilité + hook `onEvent`.
|
|
9
|
+
- `createMemoryRepositories()` ; profils `allocationsProfile()` (locker/bike/gare-locker/gym-locker/book-loan/rental).
|
|
10
|
+
- Exemples §12 exécutables : **velos**, **casiers-gare**, **casiers-salle-sport**, **showcase-3-modules** (+ tests `test-scripts/unit`, 10 verts).
|
|
11
|
+
|
|
12
|
+
### Notes
|
|
13
|
+
- Détenteur = `@mostajs/users` (holderId). Compose `booking`/`payment`/`subscriptions-plan`/`notifications` (injectés).
|
|
14
|
+
- Extraction depuis `SolutionCh/SecuAccessPro` (locker.schema/locker-event.schema/locker.repository).
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @mostajs/allocations
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
**Statut** : 0.1.0 (extraction exécutable — exemples §12 verts)
|
|
5
|
+
|
|
6
|
+
> Unités **assignables** à un détenteur (état occupé/libre + historique) — **générique** : casiers, prêts de livres,
|
|
7
|
+
> locations. Détenteur = **`@mostajs/users`**. Généralisé du module casier de `SecuAccessPro`.
|
|
8
|
+
|
|
9
|
+
## Install / Exemple minimal
|
|
10
|
+
```js
|
|
11
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile } from '@mostajs/allocations';
|
|
12
|
+
const a = createAllocations({ repositories: createMemoryRepositories() });
|
|
13
|
+
const u = await a.units.create({ code: 'V-07', group: 'Vestiaire', resourceType: allocationsProfile('gym-locker').resourceType });
|
|
14
|
+
await a.alloc.assign(u.id, memberId); // occupe
|
|
15
|
+
await a.alloc.release(u.id); // libère
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Exemples (`examples/`, exécutables)
|
|
19
|
+
- `velos/` — vélo-partage (compose payment/booking)
|
|
20
|
+
- `casiers-gare/` — consigne Gare (QR + échéance + notifications)
|
|
21
|
+
- `casiers-salle-sport/` — vestiaire gym (guard abonnement)
|
|
22
|
+
- `showcase-3-modules/` — **users + users-ui + allocations** ensemble
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node examples/casiers-salle-sport/run.mjs
|
|
26
|
+
node test-scripts/unit/allocations.test.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
API & types : voir `llms.txt`. Plan/audit : `docs/`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 00 — Audit d'extraction & proposition (cas C §4) — `@mostajs/allocations`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Date** : 2026-06-18
|
|
5
|
+
**Statut** : **proposition à discuter** (audit §9 #2 + proposition §4 — précède le scaffold).
|
|
6
|
+
**Principe** : **EXTRACTION de l'existant — NE PAS redévelopper.** Généralisation du module **casiers** de `SecuAccessPro`.
|
|
7
|
+
**Source** : `SolutionCh/SecuAccessPro` — `locker.schema.ts`, `locker-event.schema.ts`, `locker.repository.ts`, `locker-event.repository.ts`.
|
|
8
|
+
**Nom proposé** : `@mostajs/allocations` *(alternatives à valider : `lending`, `assignables`, `lockers`-generic — cf. §8)*.
|
|
9
|
+
|
|
10
|
+
> Besoin exprimé : « gestion des **casiers** d'une façon **générique** pour servir à la gestion d'autre chose :
|
|
11
|
+
> des **livres** (prêts), des **locations**, … ». → un module générique d'**unités assignables à un détenteur**.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Audit de l'existant (SecuAccessPro — modèle « casier »)
|
|
16
|
+
|
|
17
|
+
Modèle **déjà `@mostajs/orm`-natif**, propre et générique dans sa forme :
|
|
18
|
+
- **Locker** : `number, zone(A/B/C), status(available|occupied|maintenance|out_of_order), rfidLockId, lastAssignedAt` + relations `currentClient`, `currentTag`.
|
|
19
|
+
- **LockerEvent** : `eventType(assigned|released|tag_lost|maintenance_start|maintenance_end), notes, timestamp` + relations `locker, client, rfidTag, performedBy`.
|
|
20
|
+
- **LockerRepository** : `findAllWithOccupants`, `findByClient`, `countOccupied/Total`, `assign(id, clientId, tagId)`, `release(id)`, `setMaintenance/endMaintenance`.
|
|
21
|
+
|
|
22
|
+
**Constat** : c'est **déjà** le patron générique « **unité ↔ détenteur + cycle d'état + historique d'événements** ». Seuls les noms (`Locker`, `zone`, `currentClient`, `rfidLockId`) sont spécifiques. → **généraliser**, pas réécrire.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Application de la règle d'or (§0)
|
|
27
|
+
|
|
28
|
+
| Proche | Couvre | Ne couvre PAS |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `@mostajs/booking-stack` | **créneaux/agenda/réservation** (temps) | l'**unité physique ↔ détenteur**, l'état occupé/libre **persistant**, l'historique d'affectation |
|
|
31
|
+
| `@mostajs/secu` | contrôle d'accès (clients, badges, **casiers spécifiques**) | une gestion **générique** d'unités réutilisable hors accès (livres, locations) |
|
|
32
|
+
| `@mostajs/subscriptions-plan` | abonnement/facturation | l'affectation d'une unité concrète |
|
|
33
|
+
|
|
34
|
+
→ **cas A/B écartés** (aucun ne porte l'unité assignable générique) → **cas C**. (Et `secu` **consommera** `allocations` pour ses casiers — extraction propre.)
|
|
35
|
+
|
|
36
|
+
**Frontière** : `booking` = *quand* (créneau) ; `allocations` = *quelle unité, à qui, dans quel état*. Une **location** compose les deux (booking pour la période + allocations pour l'unité + `payment` pour le prix). Un **prêt de livre** = allocations + `dueAt` + `notifications` (rappel).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Périmètre `@mostajs/allocations` (généralisation)
|
|
41
|
+
|
|
42
|
+
| Entité | Champs (généralisés depuis Locker/LockerEvent) | Rôle |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| **Unit** | `code, group?(ex-zone), resourceType(locker\|book\|rental\|…), status(available\|assigned\|maintenance\|retired), lastAssignedAt, externalRef?(ex-rfidLockId), meta?` + rel `currentHolder?(User)` | l'unité assignable |
|
|
45
|
+
| **Allocation** *(état courant)* | `unitId, holderId(User), since, dueAt?, status(active\|returned\|overdue), price?` | l'affectation en cours (prêt/location/occupation) |
|
|
46
|
+
| **AllocationEvent** *(historique)* | `unitId, holderId?, action(assigned\|released\|renewed\|overdue\|maintenance_*), at, performedBy(User), notes` | journal immuable |
|
|
47
|
+
|
|
48
|
+
> **Détenteur = `@mostajs/users`** (ou une entité **dérivée** de `User` : Client, Élève, Patient…) — réutilise le socle personne (`currentClient`→`currentHolder:User`).
|
|
49
|
+
|
|
50
|
+
**API (extraite/généralisée)** : `createAllocations({repositories, users?, booking?, payment?, now?})` → `units.{create,list,findAvailable(group?),setMaintenance,retire}`, `alloc.{assign(unitId,holderId,{dueAt?,price?}), release(unitId), renew(unitId,dueAt)}`, `alloc.{findByHolder, history(unitId), countByStatus}`. *(← `assign/release/findByClient/countOccupied/findAllWithOccupants`)*.
|
|
51
|
+
|
|
52
|
+
**Composition (§10)** : `repository`/`orm` (persistance) · **`users`** (détenteur) · `booking-stack` (période de location) · `payment`/`subscriptions-plan` (prix/abonnement) · `notifications` (rappels d'échéance) · `audit` (traçabilité). **Profils** d'usage livrés : `locker`, `book-loan`, `rental`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Carte d'extraction (source → cible — *lift/adapt/generalize*)
|
|
57
|
+
|
|
58
|
+
| # | Source SecuAccessPro | Cible `mosta-allocations/` | Action |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| A1 | `locker.schema.ts` | `src/schemas/unit.schema.ts` | **GENERALIZE** : `number`→`code`, `zone`→`group`, +`resourceType`, `currentClient`→`currentHolder:User`, `rfidLockId`→`externalRef` |
|
|
61
|
+
| A2 | `locker-event.schema.ts` | `src/schemas/allocation-event.schema.ts` | **ADAPT** : `client`→`holder:User` ; +actions `renewed/overdue` |
|
|
62
|
+
| A3 | *(nouveau, mince)* état courant | `src/schemas/allocation.schema.ts` | **ADD** (dérivé du couple status+lastAssignedAt + `dueAt/price`) |
|
|
63
|
+
| A4 | `locker.repository.ts` (`assign/release/findAvailable/count`) | `src/repositories/allocations.repository.ts` | **LIFT** + renommer générique + `renew`/`overdue` |
|
|
64
|
+
| A5 | profils concrets | `src/profiles/{locker,book-loan,rental}.ts` | **ADD** (presets de `resourceType` + champs meta) |
|
|
65
|
+
|
|
66
|
+
> Aucune ligne « réécrire de zéro » : tout est **GENERALIZE/ADAPT/LIFT** depuis le code casier éprouvé.
|
|
67
|
+
|
|
68
|
+
## 5. Versions (jalons)
|
|
69
|
+
| Version | Contenu | Extraction |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| 0.1.0 | `Unit` + `AllocationEvent` + repo (`assign/release/findAvailable`) + repos mémoire + profil `locker` | A1,A2,A4 |
|
|
72
|
+
| 0.2.0 | `Allocation` (dueAt/status) + `renew/overdue` + profil `book-loan` (compose `notifications`) | A3,A5 |
|
|
73
|
+
| 0.3.0 | profil `rental` (compose `booking-stack`+`payment`) + adaptateurs `@mostajs/repository` | A5 |
|
|
74
|
+
| 1.0.0 | 14 livrables §9 + portes §11 + exemple §12 + article #17 + **migration SecuAccessPro `secu`→`allocations`** | — |
|
|
75
|
+
|
|
76
|
+
## 6. Réutilisabilité (§4.4)
|
|
77
|
+
SecuAccessPro (casiers) **+** bibliothèque/prêts (livres) **+** locations **+** tout pool d'unités (matériel, salles-clés, véhicules) → **≥ 2 consommateurs**, générique transverse.
|
|
78
|
+
|
|
79
|
+
**3 exemples ciblés** (mêmes API/entités, profil différent — cf. plan de dev §8, exemples §12) :
|
|
80
|
+
- **Vélos** (vélo-partage) : `resourceType='bike'`, station=`group`, compose `payment`+`booking`.
|
|
81
|
+
- **Casiers de la Gare** (MostaGare) : `resourceType='gare-locker'`, consigne court terme, compose `qrpanel`/`scan`+`payment`+`notifications` (échéance).
|
|
82
|
+
- **Casiers de salle de sport** (gym) : `resourceType='gym-locker'`, détenteur=membre abonné, compose `subscriptions-plan`+`scan`/`face`.
|
|
83
|
+
|
|
84
|
+
## 7. Portes §11
|
|
85
|
+
§11.1 qualité (bloquante) ; §11.2/§11.3 **informatives** (données détenteur via `users`). SemVer strict §11.8.
|
|
86
|
+
|
|
87
|
+
## 8. Questions ouvertes (validation)
|
|
88
|
+
1. **Nom** : `@mostajs/allocations` (recommandé) vs `lending` / `assignables` / `lockers`-generic ?
|
|
89
|
+
2. `Allocation` (état courant) séparé de `Unit.status`, ou dérivé du dernier `AllocationEvent` ?
|
|
90
|
+
3. `secu` consomme `allocations` pour ses casiers (migration), ou garde un alias ?
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
**Fin de l'audit d'extraction `@mostajs/allocations`.**
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 03 — Plan de développement (EXTRACTION) — `@mostajs/allocations`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Date** : 2026-06-18
|
|
5
|
+
**Statut** : proposition à discuter (livrable §9 #3). **Précède le scaffold code.**
|
|
6
|
+
**Principe** : **EXTRAIRE & GÉNÉRALISER, NE PAS REDÉVELOPPER.** Généralisation du module *casiers* de `SecuAccessPro`.
|
|
7
|
+
**Amont** : `00-AUDIT-EXTRACTION-ALLOCATIONS-18062026.md` (carte A1–A5)
|
|
8
|
+
**Source** : `SolutionCh/SecuAccessPro` (`locker.schema.ts`, `locker-event.schema.ts`, `locker.repository.ts` — déjà `@mostajs/orm`).
|
|
9
|
+
|
|
10
|
+
> Module générique d'**unités assignables à un détenteur** (état occupé/libre + historique). Le **détenteur = `@mostajs/users`** (ou entité dérivée). Couvre **casiers, prêts de livres, locations** — et les **3 exemples ciblés : vélos, casiers de Gare, casiers de salle de sport** (§8).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Périmètre & responsabilité unique
|
|
15
|
+
**Unité ↔ détenteur ↔ état ↔ historique**, **DB-agnostique** (repos injectés). **Hors périmètre (délégué)** : détenteur (`users`), créneau/agenda (`booking-stack`), prix/abonnement (`payment`/`subscriptions-plan`), rappels (`notifications`), badge/QR (`qrpanel`/`scan`), traçabilité (`audit`).
|
|
16
|
+
|
|
17
|
+
## 2. Carte d'extraction (source → cible — *generalize/adapt/lift*)
|
|
18
|
+
| # | Source SecuAccessPro | Cible `mosta-allocations/src/` | Action |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| A1 | `locker.schema.ts` | `schemas/unit.schema.ts` | **GENERALIZE** : `number`→`code`, `zone`→`group`, +`resourceType`, `currentClient`→`currentHolder:User`, `rfidLockId`→`externalRef` |
|
|
21
|
+
| A2 | `locker-event.schema.ts` | `schemas/allocation-event.schema.ts` | **ADAPT** : `client`→`holder:User` ; +actions `renewed/overdue` |
|
|
22
|
+
| A3 | (dérivé status+lastAssignedAt) | `schemas/allocation.schema.ts` | **ADD** mince : `unitId,holderId,since,dueAt?,price?,status(active/returned/overdue)` |
|
|
23
|
+
| A4 | `locker.repository.ts` (`assign/release/findAvailable/count`) | `repositories/allocations.repository.ts` | **LIFT** + générique + `renew`/`markOverdue` |
|
|
24
|
+
| A5 | (nouveau) presets | `profiles/{locker,bike,book-loan,rental}.ts` | **ADD** (resourceType + champs meta + règles par profil) |
|
|
25
|
+
|
|
26
|
+
> Aucune ligne « réécrire de zéro » : GENERALIZE/ADAPT/LIFT/ADD.
|
|
27
|
+
|
|
28
|
+
## 3. Entités
|
|
29
|
+
- **Unit** : `code, group?, resourceType, status(available|assigned|maintenance|retired), lastAssignedAt, externalRef?, meta?` + rel `currentHolder?(User)`.
|
|
30
|
+
- **Allocation** : `unitId, holderId(User), since, dueAt?, price?, status(active|returned|overdue)`.
|
|
31
|
+
- **AllocationEvent** : `unitId, holderId?, action(assigned|released|renewed|overdue|maintenance_*), at, performedBy(User), notes`.
|
|
32
|
+
|
|
33
|
+
## 4. API (extraite/généralisée)
|
|
34
|
+
`createAllocations({repositories, users?, booking?, payment?, notifications?, now?})` →
|
|
35
|
+
`units.{create,list,findAvailable(group?),setMaintenance,retire}` · `alloc.{assign(unitId,holderId,{dueAt?,price?}), release(unitId), renew(unitId,dueAt), markOverdue}` · `alloc.{findByHolder, history(unitId), countByStatus}`.
|
|
36
|
+
**Profils** : `allocationsProfile('bike'|'gare-locker'|'gym-locker'|'book-loan'|'rental')` → presets de champs/règles.
|
|
37
|
+
|
|
38
|
+
## 5. Composition (§10)
|
|
39
|
+
`repository`/`orm` (persistance, requis) · **`users`** (détenteur) · `booking-stack` (période location/vélo) · `payment`/`subscriptions-plan` (prix/abonnement) · `notifications` (échéance/retour) · `qrpanel`/`scan` (déverrouillage badge) · `audit`.
|
|
40
|
+
|
|
41
|
+
## 6. Versions (jalons)
|
|
42
|
+
| Version | Contenu | Extraction |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| 0.1.0 | `Unit`+`AllocationEvent`+repo (`assign/release/findAvailable/count`) + repos mémoire + profil `locker` | A1,A2,A4 |
|
|
45
|
+
| 0.2.0 | `Allocation` (dueAt/status) + `renew`/`markOverdue` + `notifications` (rappels) | A3 |
|
|
46
|
+
| 0.3.0 | profils **`bike` / `gare-locker` / `gym-locker`** + compose `booking`/`payment`/`subscriptions-plan` | A5 |
|
|
47
|
+
| 0.4.0 | profil `book-loan`/`rental` + adaptateurs `@mostajs/repository` prod | A5 |
|
|
48
|
+
| 1.0.0 | 14 livrables §9 + portes §11 + **3 exemples §12** + article #17 + **migration SecuAccessPro `secu`→`allocations`** | — |
|
|
49
|
+
|
|
50
|
+
## 7. Frontière `booking-stack` (rappel)
|
|
51
|
+
`booking` = *quand* (créneau/agenda) ; `allocations` = *quelle unité, à qui, état, historique*. Vélo/location **composent** les deux (période → booking) ; casier court terme = allocations seul.
|
|
52
|
+
|
|
53
|
+
## 8. Exemples §12 — **3 cas ciblés** (vérification exécutable, `examples/`)
|
|
54
|
+
|
|
55
|
+
> Même module, même API — seul le **profil** (resourceType + règles) change. Démontre la généricité.
|
|
56
|
+
|
|
57
|
+
### 8.1 `examples/velos/` — **allocation de vélos** (vélo-partage)
|
|
58
|
+
- `Unit.resourceType='bike'`, `group=station`, `externalRef=cadenas/GPS`. `assign(bike, holder, {price})` au retrait, `release` au retour (autre station → `group` mis à jour). **Compose** `payment` (tarif durée) + `booking` (réservation). **Assert** : capacité station, vélo indisponible si `assigned`, facturation durée, historique trajets.
|
|
59
|
+
|
|
60
|
+
### 8.2 `examples/casiers-gare/` — **casiers de la Gare** (MostaGare)
|
|
61
|
+
- `Unit.resourceType='gare-locker'`, `group=quai/zone`. Casier consigne court terme : `assign(casier, voyageur, {dueAt})`, `release` au départ, `markOverdue` au-delà. **Compose** `qrpanel`/`scan` (ouverture par QR) + `payment` (paiement à l'usage) + `notifications` (rappel échéance). **Assert** : libération auto en retard, paiement requis, code d'ouverture.
|
|
62
|
+
|
|
63
|
+
### 8.3 `examples/casiers-salle-sport/` — **casiers de salle de sport** (gym)
|
|
64
|
+
- `Unit.resourceType='gym-locker'`, `group=vestiaire H/F`. Détenteur = **membre** (User dérivé) avec **abonnement** : `assign(casier, membre)` à l'entrée (RFID/badge), `release` à la sortie ; accès conditionné à `subscriptions-plan` actif. **Compose** `subscriptions-plan` (abonnement valide) + `scan`/`face` (badge/visage) + `audit`. **Assert** : refus si abonnement expiré, un casier par membre actif, historique d'occupation.
|
|
65
|
+
|
|
66
|
+
> Chaque exemple : scénario headless `*.mjs` (`assert`) + README (modèle + commande). **DoD §12** : les 3 s'exécutent, assertions vertes — **prérequis de l'article #17**.
|
|
67
|
+
|
|
68
|
+
## 9. Risques & portes
|
|
69
|
+
| Risque | Mitigation |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Débordement vers `booking` | frontière §7 ; allocations ne gère pas l'agenda |
|
|
72
|
+
| Profils trop spécifiques | presets minces (resourceType+règles) ; cœur générique |
|
|
73
|
+
| Concurrence d'assignation (unité prise) | `assign` atomique côté repo (test d'intégration) |
|
|
74
|
+
|
|
75
|
+
**Portes §11** : §11.1 qualité (bloquante : build/tests, couverture ≥ 80 %) ; §11.8 SemVer ; **§11.2/§11.3 informatives** (détenteur via `users` → données perso, non bloquant).
|
|
76
|
+
|
|
77
|
+
## 10. Jalons §9
|
|
78
|
+
`#1` état de l'art (lockers/lending/bike-sharing — à rédiger + script §8) · `#2` audit (= doc audit) · **`#3` ce plan** · `#4` plan de test (T-cases assign/release/overdue/capacité + 3 profils) · `#6` `llms.txt` dès 0.1 · `#7` CHANGELOG · **§12 (3 exemples) AVANT #17**.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
**Fin du plan de dev (extraction) `@mostajs/allocations`.**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Exemple §12 — CASIERS DE LA GARE (consigne court terme). node examples/casiers-gare/run.mjs
|
|
2
|
+
// Met en évidence : @mostajs/users (voyageur) + @mostajs/allocations (profil 'gare-locker') + QR + échéance.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile } from '../../src/index.js';
|
|
5
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
6
|
+
|
|
7
|
+
const profile = allocationsProfile('gare-locker');
|
|
8
|
+
const users = createUsers({ repositories: userRepos(), qr: { generate: (id) => `GARE-QR:${id}` } });
|
|
9
|
+
|
|
10
|
+
// mock @mostajs/qrpanel + notifications (composition)
|
|
11
|
+
const reminders = [];
|
|
12
|
+
let clock = new Date('2026-06-18T08:00:00Z');
|
|
13
|
+
const a = createAllocations({
|
|
14
|
+
repositories: createMemoryRepositories(),
|
|
15
|
+
now: () => clock,
|
|
16
|
+
onEvent: (e) => { if (e.action === 'overdue') reminders.push(e.unitId); }, // compose notifications
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const c1 = await a.units.create({ code: 'CONS-12', group: 'Quai 2', resourceType: profile.resourceType });
|
|
20
|
+
const voyageur = await users.create({ firstName: 'Nadia', lastName: 'Cherif' });
|
|
21
|
+
|
|
22
|
+
// Dépôt avec échéance (paiement à l'usage simulé) + code d'ouverture QR
|
|
23
|
+
const alloc = await a.alloc.assign(c1.id, voyageur.id, { dueAt: new Date('2026-06-18T10:00:00Z'), price: 100 });
|
|
24
|
+
const card = await users.cardData(voyageur.id);
|
|
25
|
+
assert.ok(card.qr.startsWith('GARE-QR:'), 'code QR voyageur (compose qrpanel)');
|
|
26
|
+
assert.equal((await a.units.get(c1.id)).status, 'assigned');
|
|
27
|
+
|
|
28
|
+
// Le temps passe → dépassement → markOverdue (rappel notifications)
|
|
29
|
+
clock = new Date('2026-06-18T11:00:00Z');
|
|
30
|
+
const n = await a.alloc.markOverdue();
|
|
31
|
+
assert.equal(n, 1);
|
|
32
|
+
assert.equal(reminders.length, 1, 'rappel d\'échéance émis');
|
|
33
|
+
assert.equal((await a.alloc.countByStatus()).overdue, 1);
|
|
34
|
+
|
|
35
|
+
// Récupération au départ
|
|
36
|
+
await a.alloc.release(c1.id);
|
|
37
|
+
assert.equal((await a.units.get(c1.id)).status, 'available');
|
|
38
|
+
|
|
39
|
+
console.log('✅ casiers Gare — consigne OK (users + allocations + QR + échéance)');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Exemple §12 — CASIERS DE SALLE DE SPORT (gym). node examples/casiers-salle-sport/run.mjs
|
|
2
|
+
// Met en évidence : @mostajs/users (membre) + @mostajs/allocations (profil 'gym-locker') + abonnement (guard).
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile } from '../../src/index.js';
|
|
5
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
6
|
+
|
|
7
|
+
const profile = allocationsProfile('gym-locker');
|
|
8
|
+
const users = createUsers({ repositories: userRepos() });
|
|
9
|
+
|
|
10
|
+
// mock @mostajs/subscriptions-plan : abonnement actif ?
|
|
11
|
+
const subs = new Map();
|
|
12
|
+
const subscriptions = { isActive: (memberId) => subs.get(memberId) === true };
|
|
13
|
+
|
|
14
|
+
// guard : accès casier conditionné à un abonnement actif
|
|
15
|
+
const a = createAllocations({
|
|
16
|
+
repositories: createMemoryRepositories(),
|
|
17
|
+
guard: (unit, holderId) => {
|
|
18
|
+
if (!subscriptions.isActive(holderId)) throw new Error('abonnement inactif — accès refusé');
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const locker = await a.units.create({ code: 'V-07', group: 'Vestiaire H', resourceType: profile.resourceType });
|
|
23
|
+
const membre = await users.create({ firstName: 'Sofiane', lastName: 'Brahimi' });
|
|
24
|
+
|
|
25
|
+
// Abonnement expiré → refus
|
|
26
|
+
await assert.rejects(() => a.alloc.assign(locker.id, membre.id), /abonnement inactif/);
|
|
27
|
+
|
|
28
|
+
// Abonnement actif → entrée OK (badge/visage géré en amont par users/face)
|
|
29
|
+
subs.set(membre.id, true);
|
|
30
|
+
await a.alloc.assign(locker.id, membre.id);
|
|
31
|
+
assert.equal((await a.units.get(locker.id)).status, 'assigned');
|
|
32
|
+
assert.equal((await a.units.get(locker.id)).currentHolder, membre.id);
|
|
33
|
+
|
|
34
|
+
// Un casier par membre actif (le casier n'est plus disponible)
|
|
35
|
+
assert.equal((await a.units.findAvailable({ group: 'Vestiaire H' })).length, 0);
|
|
36
|
+
|
|
37
|
+
// Sortie → libération + historique
|
|
38
|
+
await a.alloc.release(locker.id);
|
|
39
|
+
assert.equal((await a.units.get(locker.id)).status, 'available');
|
|
40
|
+
const hist = await a.alloc.history(locker.id);
|
|
41
|
+
assert.deepEqual(hist.map((e) => e.action), ['assigned', 'released']);
|
|
42
|
+
|
|
43
|
+
console.log('✅ casiers salle de sport — vestiaire OK (users + allocations + abonnement)');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SHOWCASE — met en évidence LES TROIS MODULES ensemble :
|
|
2
|
+
// @mostajs/users (personne), @mostajs/users-ui (admin headless), @mostajs/allocations (casier gym).
|
|
3
|
+
// node examples/showcase-3-modules/run.mjs
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
6
|
+
import { createUsersUiController } from '../../../mosta-users-stack/mosta-users-ui/src/index.js';
|
|
7
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile } from '../../src/index.js';
|
|
8
|
+
|
|
9
|
+
// 1) @mostajs/users — service personne (socle)
|
|
10
|
+
const users = createUsers({ repositories: userRepos(), qr: { generate: (id) => `QR:${id}` } });
|
|
11
|
+
|
|
12
|
+
// 2) @mostajs/users-ui — un admin crée un MEMBRE via le contrôleur (validation incluse)
|
|
13
|
+
const ui = createUsersUiController({ service: users });
|
|
14
|
+
const res = await ui.submit({ firstName: 'Inès', lastName: 'Mansouri', email: 'ines@gym.dz', password: 'p', confirmPassword: 'p' });
|
|
15
|
+
assert.ok(res.ok, 'membre créé via users-ui');
|
|
16
|
+
const member = res.user;
|
|
17
|
+
console.log(' [users-ui] membre créé :', member.firstName, member.lastName, '— carte', (await users.cardData(member.id)).qr);
|
|
18
|
+
|
|
19
|
+
// 3) @mostajs/allocations — abonnement actif → casier de vestiaire affecté au membre
|
|
20
|
+
const subs = new Map([[member.id, true]]);
|
|
21
|
+
const gym = createAllocations({
|
|
22
|
+
repositories: createMemoryRepositories(),
|
|
23
|
+
guard: (unit, holderId) => { if (!subs.get(holderId)) throw new Error('abonnement inactif'); },
|
|
24
|
+
});
|
|
25
|
+
const locker = await gym.units.create({ code: 'V-07', group: 'Vestiaire F', resourceType: allocationsProfile('gym-locker').resourceType });
|
|
26
|
+
await gym.alloc.assign(locker.id, member.id);
|
|
27
|
+
assert.equal((await gym.units.get(locker.id)).currentHolder, member.id);
|
|
28
|
+
|
|
29
|
+
// vue admin (users-ui) : le membre apparaît, actif
|
|
30
|
+
const vm = await ui.refresh();
|
|
31
|
+
assert.equal(vm.stats.active, 1);
|
|
32
|
+
|
|
33
|
+
// sortie : libération
|
|
34
|
+
await gym.alloc.release(locker.id);
|
|
35
|
+
assert.equal((await gym.units.get(locker.id)).status, 'available');
|
|
36
|
+
|
|
37
|
+
console.log('✅ SHOWCASE — users + users-ui + allocations fonctionnent ensemble');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Exemple §12 — ALLOCATION DE VÉLOS (vélo-partage). node examples/velos/run.mjs
|
|
2
|
+
// Met en évidence : @mostajs/users (détenteur) + @mostajs/allocations (profil 'bike').
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile } from '../../src/index.js';
|
|
5
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
6
|
+
|
|
7
|
+
const profile = allocationsProfile('bike'); // resourceType='bike', group=station
|
|
8
|
+
const users = createUsers({ repositories: userRepos() });
|
|
9
|
+
|
|
10
|
+
// mock @mostajs/payment (tarif durée) — composition
|
|
11
|
+
const billed = [];
|
|
12
|
+
const payment = { charge: (holderId, amount) => billed.push({ holderId, amount }) };
|
|
13
|
+
|
|
14
|
+
const a = createAllocations({
|
|
15
|
+
repositories: createMemoryRepositories(),
|
|
16
|
+
onEvent: (e) => { if (e.action === 'released') payment.charge(e.holderId, 50); }, // tarif forfaitaire démo
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Parc : 2 vélos station Centre, 1 station Plage
|
|
20
|
+
const v1 = await a.units.create({ code: 'BIKE-001', group: 'Centre', resourceType: profile.resourceType, externalRef: 'lock-001' });
|
|
21
|
+
await a.units.create({ code: 'BIKE-002', group: 'Centre', resourceType: profile.resourceType });
|
|
22
|
+
await a.units.create({ code: 'BIKE-003', group: 'Plage', resourceType: profile.resourceType });
|
|
23
|
+
|
|
24
|
+
// Usager (détenteur via @mostajs/users)
|
|
25
|
+
const rider = await users.create({ firstName: 'Yacine', lastName: 'Saadi' });
|
|
26
|
+
|
|
27
|
+
// Retrait à Centre
|
|
28
|
+
await a.alloc.assign(v1.id, rider.id, { price: 50 });
|
|
29
|
+
assert.equal((await a.units.get(v1.id)).status, 'assigned');
|
|
30
|
+
assert.equal((await a.units.findAvailable({ group: 'Centre' })).length, 1, 'capacité station Centre');
|
|
31
|
+
|
|
32
|
+
// Vélo indisponible → assign refusé
|
|
33
|
+
await assert.rejects(() => a.alloc.assign(v1.id, rider.id), /non disponible/);
|
|
34
|
+
|
|
35
|
+
// Retour à la station Plage (group mis à jour) → facturation durée
|
|
36
|
+
await a.alloc.release(v1.id);
|
|
37
|
+
await a.repositories.units.update(v1.id, { group: 'Plage' });
|
|
38
|
+
assert.equal((await a.units.get(v1.id)).status, 'available');
|
|
39
|
+
assert.equal(billed.length, 1, 'facturé au retour (compose payment)');
|
|
40
|
+
assert.equal((await a.alloc.history(v1.id)).length, 2, 'historique assigned+released');
|
|
41
|
+
|
|
42
|
+
console.log('✅ vélos — allocation OK (users + allocations + payment)');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Démo web — interface `@mostajs/allocations` (autonome)
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Interface **dédiée** des unités assignables — casiers / vélos / consigne (application autonome, **non fusionnée**
|
|
6
|
+
avec users-ui). Zéro-build (DEVRULES §12) : importmap → modules ESM réels, état **in-memory** navigateur.
|
|
7
|
+
|
|
8
|
+
## Démarrer
|
|
9
|
+
```bash
|
|
10
|
+
./serve.sh # (ou ./serve.sh 8080)
|
|
11
|
+
```
|
|
12
|
+
Le script sert depuis la **racine commune** (`mostajs/`, car l'importmap référence `@mostajs/allocations`
|
|
13
|
+
et le module voisin `@mostajs/users` pour les détenteurs) et affiche l'URL exacte, p. ex. :
|
|
14
|
+
`http://localhost:8000/mosta-allocations/examples/web-demo/index.html`
|
|
15
|
+
|
|
16
|
+
> Lancement manuel équivalent : depuis `mostajs/`, `python3 -m http.server` puis ouvrir l'URL ci-dessus.
|
|
17
|
+
|
|
18
|
+
## Ce que ça montre
|
|
19
|
+
- Ajout d'unités par **profil** (`gym-locker`, `bike`, `gare-locker`…), parc avec statut (libre/occupé), **affectation**
|
|
20
|
+
à un détenteur et **libération**, journal d'événements — via `createAllocations()`.
|
|
21
|
+
- Les **détenteurs** sont des utilisateurs `@mostajs/users` (composition) — pas une UI de gestion d'utilisateurs ici
|
|
22
|
+
(celle-ci est l'application séparée `@mostajs/users-ui`).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Interface @mostajs/allocations (application autonome). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
import { createAllocations, createMemoryRepositories, PROFILES } from '@mostajs/allocations';
|
|
3
|
+
import { createUsers, createMemoryRepositories as userRepos } from '@mostajs/users';
|
|
4
|
+
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
const logs = [];
|
|
7
|
+
|
|
8
|
+
// Détenteurs : quelques utilisateurs de démo (composition @mostajs/users — pas une UI de gestion ici)
|
|
9
|
+
const users = createUsers({ repositories: userRepos() });
|
|
10
|
+
const seedHolders = async () => {
|
|
11
|
+
for (const n of [['Inès', 'Mansouri'], ['Yacine', 'Saadi'], ['Nadia', 'Cherif']])
|
|
12
|
+
await users.create({ firstName: n[0], lastName: n[1] });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const alloc = createAllocations({
|
|
16
|
+
repositories: createMemoryRepositories(),
|
|
17
|
+
onEvent: (e) => { logs.unshift(`${new Date().toLocaleTimeString()} · ${e.action} · ${e.unitId.slice(0, 6)}${e.holderId ? ' → ' + e.holderId.slice(0, 6) : ''}`); render(); },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// remplir la liste des profils
|
|
21
|
+
for (const k of Object.keys(PROFILES)) {
|
|
22
|
+
const o = document.createElement('option'); o.value = k; o.textContent = `${k} (${PROFILES[k].unitLabel})`; $('a-profile').appendChild(o);
|
|
23
|
+
}
|
|
24
|
+
$('a-profile').value = 'gym-locker';
|
|
25
|
+
|
|
26
|
+
$('a-add').onclick = async () => {
|
|
27
|
+
const code = $('a-code').value.trim(); if (!code) return;
|
|
28
|
+
const p = PROFILES[$('a-profile').value];
|
|
29
|
+
await alloc.units.create({ code, group: $('a-group').value.trim(), resourceType: p.resourceType });
|
|
30
|
+
$('a-code').value = ''; render();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function holdersOptions(selectedExclude) {
|
|
34
|
+
const list = await users.list({ pageSize: 100 });
|
|
35
|
+
return list.items.map((u) => `<option value="${u.id}">${users.fullName(u)}</option>`).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function render() {
|
|
39
|
+
const units = await alloc.units.list();
|
|
40
|
+
const c = await alloc.alloc.countByStatus();
|
|
41
|
+
$('a-stats').textContent = `${units.length} unités · actives ${c.active} · retournées ${c.returned} · en retard ${c.overdue}`;
|
|
42
|
+
const opts = await holdersOptions();
|
|
43
|
+
$('a-units').innerHTML = units.map((u) => `
|
|
44
|
+
<div class="unit">
|
|
45
|
+
<b>${u.code}</b> <span class="pill s-${u.status}">${u.status}</span>
|
|
46
|
+
<div class="g">${u.resourceType}${u.group ? ' · ' + u.group : ''}</div>
|
|
47
|
+
${u.status === 'available'
|
|
48
|
+
? `<select data-assign="${u.id}"><option value="">— détenteur —</option>${opts}</select>
|
|
49
|
+
<button class="sm" data-do="assign" data-id="${u.id}">Affecter</button>`
|
|
50
|
+
: u.status === 'assigned'
|
|
51
|
+
? `<div class="g">détenteur: ${u.currentHolder?.slice(0, 8)}</div><button class="sm ghost" data-do="release" data-id="${u.id}">Libérer</button>`
|
|
52
|
+
: ''}
|
|
53
|
+
</div>`).join('');
|
|
54
|
+
|
|
55
|
+
$('a-units').querySelectorAll('[data-do]').forEach((btn) => {
|
|
56
|
+
btn.onclick = async () => {
|
|
57
|
+
const id = btn.dataset.id;
|
|
58
|
+
try {
|
|
59
|
+
if (btn.dataset.do === 'assign') {
|
|
60
|
+
const sel = $('a-units').querySelector(`[data-assign="${id}"]`);
|
|
61
|
+
if (!sel.value) return;
|
|
62
|
+
await alloc.alloc.assign(id, sel.value);
|
|
63
|
+
} else { await alloc.alloc.release(id); }
|
|
64
|
+
render();
|
|
65
|
+
} catch (e) { alert(e.message); }
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
$('a-log').innerHTML = logs.slice(0, 30).map((l) => `<div>${l}</div>`).join('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await seedHolders();
|
|
72
|
+
// quelques unités de démo
|
|
73
|
+
await alloc.units.create({ code: 'V-01', group: 'Vestiaire H', resourceType: 'gym-locker' });
|
|
74
|
+
await alloc.units.create({ code: 'BIKE-01', group: 'Station Centre', resourceType: 'bike' });
|
|
75
|
+
render();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Interface — @mostajs/allocations</title>
|
|
7
|
+
<!--
|
|
8
|
+
Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
9
|
+
Démo navigateur ZÉRO-BUILD (DEVRULES §12) — interface DÉDIÉE @mostajs/allocations (application autonome).
|
|
10
|
+
Lancer : python3 -m http.server (depuis ce dossier) → http://localhost:8000
|
|
11
|
+
-->
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"@mostajs/users": "../../../mosta-users-stack/mosta-users/src/index.js",
|
|
16
|
+
"@mostajs/allocations": "../../src/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
<style>
|
|
21
|
+
:root { --card:#1e293b; --mut:#94a3b8; --ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; --acc:#38bdf8; }
|
|
22
|
+
*{box-sizing:border-box} body{margin:0;font:14px/1.5 system-ui,sans-serif;background:#0b1220;color:#e2e8f0}
|
|
23
|
+
header{padding:16px 24px;background:#0f172a;border-bottom:1px solid #334155}
|
|
24
|
+
header h1{margin:0;font-size:18px;color:var(--acc)} header small{color:var(--mut)}
|
|
25
|
+
.wrap{max-width:880px;margin:0 auto;padding:16px 24px}
|
|
26
|
+
.card{background:var(--card);border:1px solid #334155;border-radius:12px;padding:16px;margin-bottom:16px}
|
|
27
|
+
.card h2{margin:0 0 12px;font-size:15px}
|
|
28
|
+
label{display:block;font-size:12px;color:var(--mut);margin:8px 0 2px}
|
|
29
|
+
input,select{width:100%;padding:8px;border-radius:8px;border:1px solid #475569;background:#0f172a;color:#e2e8f0}
|
|
30
|
+
button{padding:7px 12px;border-radius:8px;border:0;background:var(--acc);color:#001018;font-weight:600;cursor:pointer}
|
|
31
|
+
button.ghost{background:#334155;color:#e2e8f0} button.sm{padding:4px 8px;font-size:12px}
|
|
32
|
+
.row{display:flex;gap:8px;align-items:end}.row>*{flex:1}
|
|
33
|
+
.stats{color:var(--mut);font-size:12px;margin:8px 0}
|
|
34
|
+
.units{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-top:10px}
|
|
35
|
+
.unit{border:1px solid #475569;border-radius:10px;padding:10px}.unit b{font-size:13px}.unit .g{color:var(--mut);font-size:11px}
|
|
36
|
+
.pill{padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700}
|
|
37
|
+
.s-available{background:rgba(34,197,94,.15);color:var(--ok)} .s-assigned{background:rgba(245,158,11,.15);color:var(--warn)}
|
|
38
|
+
.s-maintenance,.s-retired{background:rgba(239,68,68,.15);color:var(--bad)}
|
|
39
|
+
.log{font-family:ui-monospace,monospace;font-size:11px;color:var(--mut);max-height:140px;overflow:auto;margin-top:8px}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<header>
|
|
44
|
+
<h1>🔐 @mostajs/allocations</h1>
|
|
45
|
+
<small>Application autonome — unités assignables (casiers / vélos / consigne). État in-memory.</small>
|
|
46
|
+
</header>
|
|
47
|
+
<div class="wrap">
|
|
48
|
+
<section class="card">
|
|
49
|
+
<h2>Ajouter une unité</h2>
|
|
50
|
+
<div class="row">
|
|
51
|
+
<div><label>Code</label><input id="a-code" placeholder="V-07" /></div>
|
|
52
|
+
<div><label>Groupe</label><input id="a-group" placeholder="Vestiaire H" /></div>
|
|
53
|
+
<div><label>Profil</label><select id="a-profile"></select></div>
|
|
54
|
+
<div style="flex:0 0 auto"><button id="a-add" class="ghost">+ Ajouter</button></div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="stats" id="a-stats"></div>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section class="card">
|
|
60
|
+
<h2>Parc d'unités</h2>
|
|
61
|
+
<div class="units" id="a-units"></div>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section class="card">
|
|
65
|
+
<h2>Journal</h2>
|
|
66
|
+
<div class="log" id="a-log"></div>
|
|
67
|
+
</section>
|
|
68
|
+
</div>
|
|
69
|
+
<script type="module" src="./app.mjs"></script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Démarre la démo web @mostajs/allocations (zéro-build). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
# L'importmap référence des modules voisins (../../../) → on sert depuis la RACINE commune (mostajs/).
|
|
4
|
+
# Usage : ./serve.sh [port]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
ROOT="$(cd "$DIR/../../.." && pwd)" # mostajs/ (contient mosta-allocations + mosta-users-stack)
|
|
8
|
+
REL="${DIR#"$ROOT"/}/index.html"
|
|
9
|
+
PORT="${1:-8000}"
|
|
10
|
+
echo "📂 racine servie : $ROOT"
|
|
11
|
+
echo "🌐 ouvrir : http://localhost:$PORT/$REL"
|
|
12
|
+
cd "$ROOT"
|
|
13
|
+
exec python3 -m http.server "$PORT"
|
package/llms.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @mostajs/allocations — fiche LLM
|
|
2
|
+
|
|
3
|
+
RÔLE
|
|
4
|
+
Unités assignables génériques à un détenteur (état + historique) : casiers, prêts de livres, locations.
|
|
5
|
+
GENERALIZE du module casier de SecuAccessPro. Détenteur = @mostajs/users (holderId).
|
|
6
|
+
Compose (injectés) : booking (période), payment (prix), subscriptions-plan (abonnement), notifications (échéance), audit.
|
|
7
|
+
|
|
8
|
+
EXPORTS
|
|
9
|
+
createAllocations({ repositories, guard?, onEvent?, now? }) -> api
|
|
10
|
+
createMemoryRepositories() -> { units, allocations, events }
|
|
11
|
+
allocationsProfile(name) / PROFILES # locker|bike|gare-locker|gym-locker|book-loan|rental
|
|
12
|
+
UnitSchema, AllocationSchema, AllocationEventSchema
|
|
13
|
+
|
|
14
|
+
API
|
|
15
|
+
units.create(d) get(id) list(filter) findAvailable({group,resourceType}) setMaintenance(id) endMaintenance(id) retire(id)
|
|
16
|
+
alloc.assign(unitId,holderId,{dueAt?,price?,performedBy?}) # throw si non disponible / guard refuse
|
|
17
|
+
alloc.release(unitId) renew(unitId,dueAt) markOverdue() findByHolder(id) history(unitId) countByStatus()
|
|
18
|
+
|
|
19
|
+
TYPES
|
|
20
|
+
Unit{code,group,resourceType,status(available|assigned|maintenance|retired),lastAssignedAt,externalRef,meta,currentHolder}
|
|
21
|
+
Allocation{unitId,holderId,since,dueAt,returnedAt,price,status(active|returned|overdue)}
|
|
22
|
+
AllocationEvent{unitId,holderId,action(assigned|released|renewed|overdue|maintenance_*),at,performedBy,notes}
|
|
23
|
+
|
|
24
|
+
PROFILS / EXEMPLES (examples/)
|
|
25
|
+
velos (bike-sharing) · casiers-gare (consigne) · casiers-salle-sport (gym, guard abonnement) · showcase-3-modules
|
|
26
|
+
|
|
27
|
+
PIÈGES
|
|
28
|
+
- guard = règle d'éligibilité (ex. abonnement actif) — peut throw. assign refuse si unité non 'available'.
|
|
29
|
+
- Frontière booking-stack : booking = créneau/agenda ; allocations = unité↔détenteur+état+historique.
|
|
30
|
+
- Détenteur via @mostajs/users (ne pas redéfinir d'utilisateur).
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/allocations",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unités assignables génériques à un détenteur (état + historique) : casiers, prêts de livres, locations. Détenteur = @mostajs/users. Profils : locker/bike/gare-locker/gym-locker/book-loan/rental.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./profiles": "./src/profiles.js",
|
|
12
|
+
"./schemas": "./src/schemas.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mostajs",
|
|
16
|
+
"allocations",
|
|
17
|
+
"lockers",
|
|
18
|
+
"lending",
|
|
19
|
+
"rental",
|
|
20
|
+
"bike-sharing"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node test-scripts/unit/allocations.test.mjs",
|
|
24
|
+
"example:velos": "node examples/velos/run.mjs",
|
|
25
|
+
"example:gare": "node examples/casiers-gare/run.mjs",
|
|
26
|
+
"example:gym": "node examples/casiers-salle-sport/run.mjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @mostajs/allocations — service générique (createAllocations)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// GENERALIZE de SecuAccessPro locker.repository (assign/release/findAvailable/count).
|
|
4
|
+
// Détenteur = @mostajs/users (holderId). Compose booking/payment/notifications/subscriptions-plan (injectés).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {object} opts.repositories { units, allocations, events }
|
|
9
|
+
* @param {(unit, holderId)=>(void|Promise<void>)} [opts.guard] règle d'éligibilité (ex. abonnement actif) — peut throw
|
|
10
|
+
* @param {(event)=>void} [opts.onEvent] hook (ex. notifications)
|
|
11
|
+
* @param {()=>Date} [opts.now]
|
|
12
|
+
*/
|
|
13
|
+
export function createAllocations({ repositories, guard, onEvent, now = () => new Date() } = {}) {
|
|
14
|
+
if (!repositories?.units) throw new Error('createAllocations: repositories.units requis');
|
|
15
|
+
const { units, allocations, events } = repositories;
|
|
16
|
+
|
|
17
|
+
const log = async (e) => { const ev = await events.create({ at: now(), ...e }); onEvent?.(ev); return ev; };
|
|
18
|
+
|
|
19
|
+
const api = {
|
|
20
|
+
units: {
|
|
21
|
+
create: (d) => units.create({ status: 'available', meta: {}, currentHolder: null, ...d }),
|
|
22
|
+
get: (id) => units.findById(id),
|
|
23
|
+
list: (filter = {}) => units.find((u) => Object.entries(filter).every(([k, v]) => u[k] === v)),
|
|
24
|
+
async findAvailable({ group, resourceType } = {}) {
|
|
25
|
+
return units.find((u) => u.status === 'available'
|
|
26
|
+
&& (group === undefined || u.group === group)
|
|
27
|
+
&& (resourceType === undefined || u.resourceType === resourceType));
|
|
28
|
+
},
|
|
29
|
+
async setMaintenance(id, { performedBy } = {}) {
|
|
30
|
+
const u = await units.update(id, { status: 'maintenance' });
|
|
31
|
+
await log({ unitId: id, action: 'maintenance_start', performedBy });
|
|
32
|
+
return u;
|
|
33
|
+
},
|
|
34
|
+
async endMaintenance(id, { performedBy } = {}) {
|
|
35
|
+
const u = await units.update(id, { status: 'available' });
|
|
36
|
+
await log({ unitId: id, action: 'maintenance_end', performedBy });
|
|
37
|
+
return u;
|
|
38
|
+
},
|
|
39
|
+
retire: (id) => units.update(id, { status: 'retired' }),
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
alloc: {
|
|
43
|
+
/** Affecte une unité à un détenteur. Throw si non disponible ou guard refuse. */
|
|
44
|
+
async assign(unitId, holderId, { dueAt = null, price = null, performedBy } = {}) {
|
|
45
|
+
const u = await units.findById(unitId);
|
|
46
|
+
if (!u) throw new Error(`unité introuvable: ${unitId}`);
|
|
47
|
+
if (u.status !== 'available') throw new Error(`unité non disponible (${u.status}): ${u.code}`);
|
|
48
|
+
if (guard) await guard(u, holderId); // ex. abonnement actif (gym)
|
|
49
|
+
await units.update(unitId, { status: 'assigned', currentHolder: holderId, lastAssignedAt: now() });
|
|
50
|
+
const a = await allocations.create({ unitId, holderId, since: now(), dueAt, price, status: 'active' });
|
|
51
|
+
await log({ unitId, holderId, action: 'assigned', performedBy });
|
|
52
|
+
return a;
|
|
53
|
+
},
|
|
54
|
+
async release(unitId, { performedBy } = {}) {
|
|
55
|
+
const a = (await allocations.find((x) => x.unitId === unitId && x.status !== 'returned'))[0];
|
|
56
|
+
if (!a) throw new Error(`aucune allocation active pour ${unitId}`);
|
|
57
|
+
await allocations.update(a.id, { status: 'returned', returnedAt: now() });
|
|
58
|
+
await units.update(unitId, { status: 'available', currentHolder: null });
|
|
59
|
+
await log({ unitId, holderId: a.holderId, action: 'released', performedBy });
|
|
60
|
+
return units.findById(unitId);
|
|
61
|
+
},
|
|
62
|
+
async renew(unitId, dueAt, { performedBy } = {}) {
|
|
63
|
+
const a = (await allocations.find((x) => x.unitId === unitId && x.status === 'active'))[0];
|
|
64
|
+
if (!a) throw new Error(`aucune allocation active pour ${unitId}`);
|
|
65
|
+
const upd = await allocations.update(a.id, { dueAt });
|
|
66
|
+
await log({ unitId, holderId: a.holderId, action: 'renewed', performedBy });
|
|
67
|
+
return upd;
|
|
68
|
+
},
|
|
69
|
+
/** Passe en `overdue` les allocations actives dont dueAt est dépassé. */
|
|
70
|
+
async markOverdue() {
|
|
71
|
+
const t = now().getTime();
|
|
72
|
+
const due = await allocations.find((x) => x.status === 'active' && x.dueAt && new Date(x.dueAt).getTime() < t);
|
|
73
|
+
for (const a of due) {
|
|
74
|
+
await allocations.update(a.id, { status: 'overdue' });
|
|
75
|
+
await log({ unitId: a.unitId, holderId: a.holderId, action: 'overdue' });
|
|
76
|
+
}
|
|
77
|
+
return due.length;
|
|
78
|
+
},
|
|
79
|
+
findByHolder: (holderId) => allocations.find((x) => x.holderId === holderId),
|
|
80
|
+
history: (unitId) => events.find((e) => e.unitId === unitId),
|
|
81
|
+
async countByStatus() {
|
|
82
|
+
return {
|
|
83
|
+
active: await allocations.count((x) => x.status === 'active'),
|
|
84
|
+
returned: await allocations.count((x) => x.status === 'returned'),
|
|
85
|
+
overdue: await allocations.count((x) => x.status === 'overdue'),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
repositories,
|
|
91
|
+
};
|
|
92
|
+
return api;
|
|
93
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @mostajs/allocations — point d'entrée
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
export { createAllocations } from './allocations.js';
|
|
4
|
+
export { createMemoryRepositories } from './memory-repo.js';
|
|
5
|
+
export { UnitSchema, AllocationSchema, AllocationEventSchema } from './schemas.js';
|
|
6
|
+
export { allocationsProfile, PROFILES } from './profiles.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @mostajs/allocations — repositories mémoire (tests/démo ; prod via @mostajs/repository)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
function makeCollection() {
|
|
4
|
+
const items = new Map();
|
|
5
|
+
return {
|
|
6
|
+
async create(data) {
|
|
7
|
+
const id = data.id || (globalThis.crypto?.randomUUID?.() ?? String(Date.now() + Math.random()));
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const row = { id, createdAt: now, updatedAt: now, ...data };
|
|
10
|
+
items.set(id, row); return { ...row };
|
|
11
|
+
},
|
|
12
|
+
async findById(id) { const r = items.get(id); return r ? { ...r } : null; },
|
|
13
|
+
async update(id, patch) { const r = items.get(id); if (!r) return null; const row = { ...r, ...patch, updatedAt: new Date() }; items.set(id, row); return { ...row }; },
|
|
14
|
+
async remove(id) { return items.delete(id); },
|
|
15
|
+
async find(p = () => true) { return [...items.values()].filter(p).map((r) => ({ ...r })); },
|
|
16
|
+
async count(p = () => true) { return [...items.values()].filter(p).length; },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createMemoryRepositories() {
|
|
20
|
+
return { units: makeCollection(), allocations: makeCollection(), events: makeCollection() };
|
|
21
|
+
}
|
package/src/profiles.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @mostajs/allocations — profils (presets resourceType + libellés). Même cœur, usage différent.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
export const PROFILES = {
|
|
4
|
+
'locker': { resourceType: 'locker', groupLabel: 'zone', unitLabel: 'casier' },
|
|
5
|
+
'bike': { resourceType: 'bike', groupLabel: 'station', unitLabel: 'vélo' },
|
|
6
|
+
'gare-locker': { resourceType: 'gare-locker', groupLabel: 'quai/zone', unitLabel: 'casier de consigne' },
|
|
7
|
+
'gym-locker': { resourceType: 'gym-locker', groupLabel: 'vestiaire', unitLabel: 'casier de vestiaire' },
|
|
8
|
+
'book-loan': { resourceType: 'book', groupLabel: 'rayon', unitLabel: 'exemplaire' },
|
|
9
|
+
'rental': { resourceType: 'rental', groupLabel: 'catégorie', unitLabel: 'article' },
|
|
10
|
+
};
|
|
11
|
+
export function allocationsProfile(name) {
|
|
12
|
+
const p = PROFILES[name];
|
|
13
|
+
if (!p) throw new Error(`profil inconnu: ${name} (cf. PROFILES)`);
|
|
14
|
+
return p;
|
|
15
|
+
}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @mostajs/allocations — schémas (EntitySchema @mostajs/orm)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// GENERALIZE de SecuAccessPro locker.schema.ts + locker-event.schema.ts (extraction, pas réécriture).
|
|
4
|
+
|
|
5
|
+
export const UnitSchema = {
|
|
6
|
+
name: 'Unit',
|
|
7
|
+
collection: 'units',
|
|
8
|
+
timestamps: true,
|
|
9
|
+
fields: {
|
|
10
|
+
code: { type: 'string', required: true }, // ex-`number`
|
|
11
|
+
group: { type: 'string' }, // ex-`zone` (station/quai/vestiaire)
|
|
12
|
+
resourceType: { type: 'string', required: true }, // locker|bike|gare-locker|gym-locker|book|rental
|
|
13
|
+
status: { type: 'string', enum: ['available', 'assigned', 'maintenance', 'retired'], default: 'available' },
|
|
14
|
+
lastAssignedAt: { type: 'date', default: null },
|
|
15
|
+
externalRef: { type: 'string' }, // ex-`rfidLockId`/cadenas
|
|
16
|
+
meta: { type: 'object', default: () => ({}) },
|
|
17
|
+
currentHolder: { type: 'string', default: null }, // userId (@mostajs/users)
|
|
18
|
+
},
|
|
19
|
+
indexes: [{ fields: { resourceType: 'asc' } }, { fields: { group: 'asc' } }, { fields: { status: 'asc' } }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const AllocationSchema = {
|
|
23
|
+
name: 'Allocation',
|
|
24
|
+
collection: 'allocations',
|
|
25
|
+
timestamps: true,
|
|
26
|
+
fields: {
|
|
27
|
+
unitId: { type: 'string', required: true },
|
|
28
|
+
holderId: { type: 'string', required: true }, // userId
|
|
29
|
+
since: { type: 'date', default: 'now' },
|
|
30
|
+
dueAt: { type: 'date', default: null },
|
|
31
|
+
returnedAt: { type: 'date', default: null },
|
|
32
|
+
price: { type: 'number', default: null },
|
|
33
|
+
status: { type: 'string', enum: ['active', 'returned', 'overdue'], default: 'active' },
|
|
34
|
+
},
|
|
35
|
+
indexes: [{ fields: { unitId: 'asc' } }, { fields: { holderId: 'asc' } }, { fields: { status: 'asc' } }],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const AllocationEventSchema = {
|
|
39
|
+
name: 'AllocationEvent',
|
|
40
|
+
collection: 'allocation_events',
|
|
41
|
+
timestamps: false,
|
|
42
|
+
fields: {
|
|
43
|
+
unitId: { type: 'string', required: true },
|
|
44
|
+
holderId: { type: 'string', default: null },
|
|
45
|
+
action: { type: 'string', enum: ['assigned', 'released', 'renewed', 'overdue', 'maintenance_start', 'maintenance_end'], required: true },
|
|
46
|
+
at: { type: 'date', default: 'now' },
|
|
47
|
+
performedBy: { type: 'string', default: null },
|
|
48
|
+
notes: { type: 'string' },
|
|
49
|
+
},
|
|
50
|
+
indexes: [{ fields: { unitId: 'asc' } }, { fields: { at: 'desc' } }],
|
|
51
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @mostajs/allocations — tests unitaires (DEVRULES §5 : test-scripts/, rejouables, committés).
|
|
2
|
+
// node test-scripts/unit/allocations.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createAllocations, createMemoryRepositories, allocationsProfile, PROFILES } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
let pass = 0; const test = async (n, fn) => { await fn(); pass++; console.log(' ✓', n); };
|
|
7
|
+
const mk = (opts = {}) => createAllocations({ repositories: createMemoryRepositories(), ...opts });
|
|
8
|
+
|
|
9
|
+
await test('units.create: status available par défaut', async () => {
|
|
10
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'locker' });
|
|
11
|
+
assert.equal(u.status, 'available'); assert.equal(u.currentHolder, null);
|
|
12
|
+
});
|
|
13
|
+
await test('assign: occupe l\'unité + crée allocation + event', async () => {
|
|
14
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'locker' });
|
|
15
|
+
await a.alloc.assign(u.id, 'holder-1');
|
|
16
|
+
assert.equal((await a.units.get(u.id)).status, 'assigned');
|
|
17
|
+
assert.equal((await a.units.get(u.id)).currentHolder, 'holder-1');
|
|
18
|
+
assert.deepEqual((await a.alloc.history(u.id)).map((e) => e.action), ['assigned']);
|
|
19
|
+
});
|
|
20
|
+
await test('assign sur unité non disponible → erreur', async () => {
|
|
21
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'locker' });
|
|
22
|
+
await a.alloc.assign(u.id, 'h1');
|
|
23
|
+
await assert.rejects(() => a.alloc.assign(u.id, 'h2'), /non disponible/);
|
|
24
|
+
});
|
|
25
|
+
await test('guard refuse (ex. abonnement)', async () => {
|
|
26
|
+
const a = mk({ guard: () => { throw new Error('abonnement inactif'); } });
|
|
27
|
+
const u = await a.units.create({ code: 'V1', resourceType: 'gym-locker' });
|
|
28
|
+
await assert.rejects(() => a.alloc.assign(u.id, 'h1'), /abonnement inactif/);
|
|
29
|
+
});
|
|
30
|
+
await test('release: libère + historise', async () => {
|
|
31
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'locker' });
|
|
32
|
+
await a.alloc.assign(u.id, 'h1'); await a.alloc.release(u.id);
|
|
33
|
+
assert.equal((await a.units.get(u.id)).status, 'available');
|
|
34
|
+
assert.deepEqual((await a.alloc.history(u.id)).map((e) => e.action), ['assigned', 'released']);
|
|
35
|
+
});
|
|
36
|
+
await test('findAvailable par group/resourceType', async () => {
|
|
37
|
+
const a = mk();
|
|
38
|
+
await a.units.create({ code: 'B1', group: 'Centre', resourceType: 'bike' });
|
|
39
|
+
const b2 = await a.units.create({ code: 'B2', group: 'Centre', resourceType: 'bike' });
|
|
40
|
+
await a.alloc.assign(b2.id, 'h1');
|
|
41
|
+
assert.equal((await a.units.findAvailable({ group: 'Centre', resourceType: 'bike' })).length, 1);
|
|
42
|
+
});
|
|
43
|
+
await test('markOverdue: échéance dépassée', async () => {
|
|
44
|
+
let clock = new Date('2026-01-01T00:00:00Z');
|
|
45
|
+
const a = mk({ now: () => clock }); const u = await a.units.create({ code: 'C1', resourceType: 'gare-locker' });
|
|
46
|
+
await a.alloc.assign(u.id, 'h1', { dueAt: new Date('2026-01-01T01:00:00Z') });
|
|
47
|
+
clock = new Date('2026-01-01T02:00:00Z');
|
|
48
|
+
assert.equal(await a.alloc.markOverdue(), 1);
|
|
49
|
+
assert.equal((await a.alloc.countByStatus()).overdue, 1);
|
|
50
|
+
});
|
|
51
|
+
await test('renew: repousse l\'échéance', async () => {
|
|
52
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'book' });
|
|
53
|
+
await a.alloc.assign(u.id, 'h1', { dueAt: new Date('2026-01-10') });
|
|
54
|
+
const r = await a.alloc.renew(u.id, new Date('2026-02-10'));
|
|
55
|
+
assert.equal(new Date(r.dueAt).getMonth(), 1);
|
|
56
|
+
});
|
|
57
|
+
await test('maintenance start/end', async () => {
|
|
58
|
+
const a = mk(); const u = await a.units.create({ code: 'C1', resourceType: 'locker' });
|
|
59
|
+
await a.units.setMaintenance(u.id); assert.equal((await a.units.get(u.id)).status, 'maintenance');
|
|
60
|
+
await a.units.endMaintenance(u.id); assert.equal((await a.units.get(u.id)).status, 'available');
|
|
61
|
+
});
|
|
62
|
+
await test('profils disponibles (velos/gare/gym)', async () => {
|
|
63
|
+
assert.equal(allocationsProfile('bike').resourceType, 'bike');
|
|
64
|
+
assert.equal(allocationsProfile('gare-locker').resourceType, 'gare-locker');
|
|
65
|
+
assert.equal(allocationsProfile('gym-locker').resourceType, 'gym-locker');
|
|
66
|
+
assert.ok(PROFILES['book-loan'] && PROFILES['rental']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(`\n✅ @mostajs/allocations — ${pass} tests OK`);
|