feat(r3.1): socle backend gestion — stock dérivé, BC, coûts figés sur OT

- migration r3_gestion (7 tables) : Partner, Part (SANS colonne de
  quantité), StockMovement (signé, tracé, PU figé), PurchaseOrder/Line,
  LaborTime (taux figé), Document (R3.2) ; User.hourlyRate administrable
- API (66 opérations) : tiers ; pièces (stock = Σ mouvements, alerte sous
  seuil) ; entrée/ajustement (motif requis, stock jamais négatif, en
  transaction) ; BC Brouillon→Envoyé→Reçu (la réception crée les RECEIPT
  et met à jour lastUnitPrice) ; consommation sur OT (stock suffisant,
  PRIX FIGÉ) ; main-d'œuvre (TAUX FIGÉ, refus si taux non défini) ;
  WorkOrderDetail.costs ; coûts verrouillés après clôture (409)
- seed : tiers/pièces/BC/taux de la maquette — OT-0341 = 505 MAD (testé)
- durcissement : références OT/DEM/BC/P par SÉQUENCES Postgres (nextval)
  — fin des courses « max+1 » (500 sporadiques sous charge parallèle) ;
  3 runs Jest complets consécutifs verts
- 55 tests (92 % stmts / 74,9 % branches) dont la recette officielle :
  consommer sous seuil → BC → réception → réappro, prix/taux figés prouvés

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 20:42:03 +01:00
parent d5041c6f05
commit dfdf8f7c18
33 changed files with 5361 additions and 717 deletions

View File

@@ -44,6 +44,23 @@ import {
PortalRequestCreateSchema,
PortalRequestStatusSchema,
} from './schemas/portail';
import {
ConsumePartSchema,
LaborTimeCreateSchema,
PartCreateSchema,
PartDetailSchema,
PartnerCreateSchema,
PartnerSchema,
PartnersResponseSchema,
PartnerUpdateSchema,
PartsResponseSchema,
PartUpdateSchema,
PurchaseOrderCreateSchema,
PurchaseOrderSchema,
PurchaseOrdersResponseSchema,
PurchaseOrderTransitionSchema,
StockMovementCreateSchema,
} from './schemas/gestion';
import {
MeterReadingCreateSchema,
MetersResponseSchema,
@@ -419,6 +436,179 @@ export const API_CONTRACT: ApiOperation[] = [
404: { description: 'Inconnue' },
},
},
// ————— R3 · Gestion (stock, achats, tiers, coûts) —————
{
operationId: 'listPartners',
method: 'get',
path: '/partners',
summary: 'Tiers (fournisseurs, syndics) — permission PURCHASE_ORDERS',
tags: ['partners'],
responses: {
200: { description: 'Liste', name: 'PartnersResponse', schema: PartnersResponseSchema },
},
},
{
operationId: 'createPartner',
method: 'post',
path: '/partners',
summary: 'Créer un tiers',
tags: ['partners'],
request: { name: 'PartnerCreate', schema: PartnerCreateSchema },
responses: {
201: { description: 'Créé', name: 'Partner', schema: PartnerSchema },
409: { description: 'Nom déjà utilisé' },
},
},
{
operationId: 'updatePartner',
method: 'patch',
path: '/partners/{id}',
summary: 'Modifier / (dés)activer un tiers',
tags: ['partners'],
pathParams: ['id'],
request: { name: 'PartnerUpdate', schema: PartnerUpdateSchema },
responses: {
200: { description: 'Mis à jour', name: 'Partner', schema: PartnerSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'listParts',
method: 'get',
path: '/parts',
summary: 'Stock de pièces — quantités DÉRIVÉES des mouvements',
tags: ['parts'],
responses: {
200: { description: 'Liste', name: 'PartsResponse', schema: PartsResponseSchema },
},
},
{
operationId: 'getPart',
method: 'get',
path: '/parts/{id}',
summary: 'Fiche pièce : les mouvements SONT le stock',
tags: ['parts'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'PartDetail', schema: PartDetailSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'createPart',
method: 'post',
path: '/parts',
summary: 'Créer une pièce (référence P-#### générée)',
tags: ['parts'],
request: { name: 'PartCreate', schema: PartCreateSchema },
responses: {
201: { description: 'Créée', name: 'PartDetail', schema: PartDetailSchema },
},
},
{
operationId: 'updatePart',
method: 'patch',
path: '/parts/{id}',
summary: 'Modifier une pièce (désignation, seuil, fournisseur…)',
tags: ['parts'],
pathParams: ['id'],
request: { name: 'PartUpdate', schema: PartUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'PartDetail', schema: PartDetailSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'addStockMovement',
method: 'post',
path: '/parts/{id}/movements',
summary: 'Entrée manuelle (+) ou ajustement (± motif REQUIS) — jamais de saisie de stock',
tags: ['parts'],
pathParams: ['id'],
request: { name: 'StockMovementCreate', schema: StockMovementCreateSchema },
responses: {
201: { description: 'Mouvement tracé', name: 'PartDetail', schema: PartDetailSchema },
409: { description: 'Le stock ne peut pas devenir négatif' },
},
},
{
operationId: 'listPurchaseOrders',
method: 'get',
path: '/purchase-orders',
summary: 'Bons de commande',
tags: ['purchase-orders'],
responses: {
200: {
description: 'Liste',
name: 'PurchaseOrdersResponse',
schema: PurchaseOrdersResponseSchema,
},
},
},
{
operationId: 'getPurchaseOrder',
method: 'get',
path: '/purchase-orders/{id}',
summary: 'Fiche BC (lignes, total)',
tags: ['purchase-orders'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'createPurchaseOrder',
method: 'post',
path: '/purchase-orders',
summary: 'Créer un BC (brouillon)',
tags: ['purchase-orders'],
request: { name: 'PurchaseOrderCreate', schema: PurchaseOrderCreateSchema },
responses: {
201: { description: 'Créé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
400: { description: 'Fournisseur ou pièce invalide' },
},
},
{
operationId: 'transitionPurchaseOrder',
method: 'post',
path: '/purchase-orders/{id}/transition',
summary: 'Envoyer / réceptionner (→ entrées de stock, PU figés) / annuler',
tags: ['purchase-orders'],
pathParams: ['id'],
request: { name: 'PurchaseOrderTransition', schema: PurchaseOrderTransitionSchema },
responses: {
200: { description: 'État changé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
409: { description: 'Transition interdite' },
},
},
{
operationId: 'consumePart',
method: 'post',
path: '/work-orders/{id}/consume-part',
summary: 'Consommer une pièce sur lOT — stock décrémenté, PRIX FIGÉ',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'ConsumePart', schema: ConsumePartSchema },
responses: {
201: { description: 'Consommée', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Stock insuffisant ou prix inconnu' },
},
},
{
operationId: 'addLaborTime',
method: 'post',
path: '/work-orders/{id}/labor',
summary: 'Saisir de la main-dœuvre — TAUX FIGÉ à la saisie',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'LaborTimeCreate', schema: LaborTimeCreateSchema },
responses: {
201: { description: 'Saisie', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Taux horaire non défini pour cette personne' },
},
},
// ————— R2 · Portail public (QR cabine — aucun compte) —————
{
operationId: 'getPortalAsset',