@primocaredentgroup/prescriptions-component 0.1.5 → 0.1.7

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.
Files changed (40) hide show
  1. package/README.md +16 -0
  2. package/dist/_internal/component/_generated/component.d.ts +8 -0
  3. package/dist/_internal/component/_generated/component.d.ts.map +1 -1
  4. package/dist/_internal/component/functions.d.ts +1 -1
  5. package/dist/_internal/component/functions.d.ts.map +1 -1
  6. package/dist/_internal/component/functions.js +1 -1
  7. package/dist/_internal/component/functions.js.map +1 -1
  8. package/dist/convex/lib/auth.d.ts +5 -0
  9. package/dist/convex/lib/auth.d.ts.map +1 -1
  10. package/dist/convex/lib/auth.js +36 -0
  11. package/dist/convex/lib/auth.js.map +1 -1
  12. package/dist/convex/mutations/digitalAssets.d.ts.map +1 -1
  13. package/dist/convex/mutations/digitalAssets.js +20 -61
  14. package/dist/convex/mutations/digitalAssets.js.map +1 -1
  15. package/dist/convex/mutations/operational.d.ts.map +1 -1
  16. package/dist/convex/mutations/operational.js +19 -54
  17. package/dist/convex/mutations/operational.js.map +1 -1
  18. package/dist/convex/mutations/phases.d.ts.map +1 -1
  19. package/dist/convex/mutations/phases.js +20 -52
  20. package/dist/convex/mutations/phases.js.map +1 -1
  21. package/dist/convex/mutations/prescriptions.d.ts +16 -0
  22. package/dist/convex/mutations/prescriptions.d.ts.map +1 -1
  23. package/dist/convex/mutations/prescriptions.js +203 -229
  24. package/dist/convex/mutations/prescriptions.js.map +1 -1
  25. package/dist/convex/mutations/syncJobs.d.ts.map +1 -1
  26. package/dist/convex/mutations/syncJobs.js +6 -16
  27. package/dist/convex/mutations/syncJobs.js.map +1 -1
  28. package/dist/convex/prescriptions/fields.d.ts.map +1 -1
  29. package/dist/convex/prescriptions/fields.js +6 -7
  30. package/dist/convex/prescriptions/fields.js.map +1 -1
  31. package/dist/convex/queries/dynamicFields.d.ts.map +1 -1
  32. package/dist/convex/queries/dynamicFields.js +2 -3
  33. package/dist/convex/queries/dynamicFields.js.map +1 -1
  34. package/dist/convex/schema.d.ts +2 -2
  35. package/dist/convex/schema.d.ts.map +1 -1
  36. package/dist/convex/schema.js +2 -2
  37. package/dist/convex/schema.js.map +1 -1
  38. package/dist/convex/types.d.ts +1 -1
  39. package/dist/convex/types.d.ts.map +1 -1
  40. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import { mutation } from "../_generated/server";
2
2
  import { ConvexError, v } from "convex/values";
3
3
  import { computeClinicalHash, generateIdempotencyKey } from "../lib/utils";
4
- import { validateClinicalData, canUserPerformAction, validateDigitalRequirements } from "../lib/validation";
5
- import { requireIdentityUser, assertPrescriptionAccess } from "../lib/auth";
4
+ import { validateClinicalData, validateDigitalRequirements } from "../lib/validation";
5
+ import { requireIdentityOrThrow } from "../lib/auth";
6
6
  import { assertDynamicRequiredComplete } from "../lib/dynamicFieldsStrict";
7
7
  import { normalizeClinicalDraftInput } from "../lib/clinicalNormalize";
8
8
  // ============================================
@@ -30,30 +30,11 @@ const calendarDataArgsValidator = v.object({
30
30
  nextAppointmentPhaseId: v.optional(v.string()),
31
31
  nextAppointmentPhaseName: v.optional(v.string()),
32
32
  });
33
- async function requireAuthenticatedUserOrThrow(ctx) {
34
- try {
35
- return await requireIdentityUser(ctx);
36
- }
37
- catch (error) {
38
- if (error instanceof ConvexError) {
39
- throw error;
40
- }
41
- throw new ConvexError({
42
- code: "UNAUTHORIZED",
43
- message: "Accesso non autorizzato",
44
- });
45
- }
46
- }
47
- function assertPrescriptionAccessOrThrow(user, prescription) {
48
- try {
49
- assertPrescriptionAccess(user, prescription);
50
- }
51
- catch (error) {
52
- throw new ConvexError({
53
- code: "FORBIDDEN",
54
- message: error instanceof Error ? error.message : "Forbidden",
55
- });
56
- }
33
+ function getActorFromIdentity(identity) {
34
+ return {
35
+ actorUserId: identity.subject,
36
+ actorRole: "HOST",
37
+ };
57
38
  }
58
39
  // ============================================
59
40
  // PRESCRIPTION MUTATIONS
@@ -74,7 +55,8 @@ export const createDraft = mutation({
74
55
  },
75
56
  handler: async (ctx, args) => {
76
57
  const now = Date.now();
77
- const user = await requireAuthenticatedUserOrThrow(ctx);
58
+ const identity = await requireIdentityOrThrow(ctx);
59
+ const actor = getActorFromIdentity(identity);
78
60
  // 1. VINCOLO: Verifica unicità pdcItemId
79
61
  const existingPrescriptions = await ctx.db
80
62
  .query("prescriptions")
@@ -85,23 +67,7 @@ export const createDraft = mutation({
85
67
  if (activePrescription) {
86
68
  throw new Error(`Esiste già una prescrizione attiva per questo PDC Item (ID: ${activePrescription._id})`);
87
69
  }
88
- // 2. Verifica ruolo + ownership clinic
89
- const canCreate = user.role === "SECRETARY" ||
90
- user.adminRole === "admin" ||
91
- user.adminRole === "superadmin";
92
- if (!canCreate) {
93
- throw new ConvexError({
94
- code: "FORBIDDEN",
95
- message: "Utente non autorizzato a creare prescrizioni",
96
- });
97
- }
98
- if (user.clinicId !== args.clinicId && user.adminRole !== "admin" && user.adminRole !== "superadmin") {
99
- throw new ConvexError({
100
- code: "FORBIDDEN",
101
- message: "Clinic non autorizzata per l'utente autenticato",
102
- });
103
- }
104
- // 3. Carica flow attivo
70
+ // 2. Carica flow attivo
105
71
  const flow = await ctx.db
106
72
  .query("flows")
107
73
  .withIndex("by_flowKey_status", (q) => q.eq("flowKey", args.flowKey).eq("status", "ACTIVE"))
@@ -125,7 +91,7 @@ export const createDraft = mutation({
125
91
  flowKey: args.flowKey,
126
92
  flowVersion: flow.version,
127
93
  status: "DRAFT",
128
- createdByUserId: user._id,
94
+ createdByUserId: actor.actorUserId,
129
95
  createdAt: now,
130
96
  updatedAt: now,
131
97
  coreRefs: {},
@@ -180,18 +146,142 @@ export const createDraft = mutation({
180
146
  operationalDataId: 0, // Will be backfilled
181
147
  prescriptionRef: prescriptionId,
182
148
  });
183
- // 9. Audit
149
+ // 8. Audit
184
150
  await ctx.db.insert("auditEvents", {
185
151
  auditEventId: 0, // Will be backfilled
186
152
  prescriptionRef: prescriptionId,
187
153
  at: now,
188
- actorUserId: user._id,
189
- actorRole: user.role,
154
+ actorUserId: actor.actorUserId,
155
+ actorRole: actor.actorRole,
190
156
  type: "PRESCRIPTION_CREATED",
191
157
  payload: {
192
158
  pdcItemId: args.pdcItemId,
193
159
  flowKey: args.flowKey,
194
160
  flowVersion: flow.version,
161
+ actorEmail: identity.email,
162
+ },
163
+ });
164
+ return { prescriptionId, revisionId };
165
+ },
166
+ });
167
+ /**
168
+ * Crea una bozza di prescrizione quando chiamata dal backend (es. da appointments dell'host).
169
+ * NON richiede identity: ctx.runMutation da un'altra mutation non propaga il token utente.
170
+ * Usa actor HOST_BACKEND per audit.
171
+ */
172
+ export const createDraftFromBackend = mutation({
173
+ args: {
174
+ clinicId: v.string(),
175
+ doctorId: v.string(),
176
+ patientId: v.string(),
177
+ pdcItemId: v.string(),
178
+ listinoId: v.string(),
179
+ flowKey: v.string(),
180
+ },
181
+ handler: async (ctx, args) => {
182
+ const now = Date.now();
183
+ const actor = { actorUserId: "HOST_BACKEND", actorRole: "HOST" };
184
+ // 1. VINCOLO: Verifica unicità pdcItemId
185
+ const existingPrescriptions = await ctx.db
186
+ .query("prescriptions")
187
+ .withIndex("by_pdcItemId", (q) => q.eq("pdcItemId", args.pdcItemId))
188
+ .collect();
189
+ const activeStatuses = ["DRAFT", "PENDING_DOCTOR", "SIGNED", "SYNCED", "ERROR"];
190
+ const activePrescription = existingPrescriptions.find((p) => activeStatuses.includes(p.status));
191
+ if (activePrescription) {
192
+ throw new Error(`Esiste già una prescrizione attiva per questo PDC Item (ID: ${activePrescription._id})`);
193
+ }
194
+ // 2. Carica flow attivo
195
+ const flow = await ctx.db
196
+ .query("flows")
197
+ .withIndex("by_flowKey_status", (q) => q.eq("flowKey", args.flowKey).eq("status", "ACTIVE"))
198
+ .first();
199
+ if (!flow) {
200
+ throw new Error(`Flow "${args.flowKey}" non trovato o non attivo`);
201
+ }
202
+ // 3. Carica ruleset attivo (se presente)
203
+ const activeRuleset = await ctx.db
204
+ .query("activeRulesets")
205
+ .withIndex("by_key", (q) => q.eq("key", "global"))
206
+ .first();
207
+ // 4. Crea prescrizione
208
+ const prescriptionId = await ctx.db.insert("prescriptions", {
209
+ prescriptionId: 0,
210
+ clinicId: args.clinicId,
211
+ doctorId: args.doctorId,
212
+ patientId: args.patientId,
213
+ pdcItemId: args.pdcItemId,
214
+ listinoId: args.listinoId,
215
+ flowKey: args.flowKey,
216
+ flowVersion: flow.version,
217
+ status: "DRAFT",
218
+ createdByUserId: actor.actorUserId,
219
+ createdAt: now,
220
+ updatedAt: now,
221
+ coreRefs: {},
222
+ labRefs: {},
223
+ rulesetVersionId: activeRuleset?.versionId,
224
+ valuesByFieldId: "{}",
225
+ });
226
+ // 5. Crea revisione clinica iniziale (vuota)
227
+ const initialClinicalData = {
228
+ patient: { corePatientId: args.patientId },
229
+ clinic: { coreClinicId: args.clinicId },
230
+ doctor: { coreDoctorId: args.doctorId },
231
+ prostheticService: {
232
+ pdcItemId: args.pdcItemId,
233
+ listinoId: args.listinoId,
234
+ serviceCode: "",
235
+ serviceLabel: "",
236
+ },
237
+ };
238
+ const clinicalHash = await computeClinicalHash(initialClinicalData);
239
+ const revisionId = await ctx.db.insert("clinicalRevisions", {
240
+ clinicalRevisionId: 0,
241
+ prescriptionRef: prescriptionId,
242
+ revisionNumber: 1,
243
+ clinicalData: initialClinicalData,
244
+ clinicalHash,
245
+ signature: null,
246
+ frozen: false,
247
+ createdAt: now,
248
+ });
249
+ // 6. Aggiorna prescrizione con riferimento alla revisione
250
+ await ctx.db.patch(prescriptionId, {
251
+ latestClinicalRevisionId: revisionId,
252
+ });
253
+ // 7. Inizializza phase instances
254
+ for (let i = 0; i < flow.definition.phases.length; i++) {
255
+ const phase = flow.definition.phases[i];
256
+ await ctx.db.insert("phaseInstances", {
257
+ phaseInstanceId: 0,
258
+ prescriptionRef: prescriptionId,
259
+ phaseTypeKey: phase.phaseTypeKey,
260
+ ordinal: i,
261
+ iteration: 1,
262
+ status: "NOT_STARTED",
263
+ createdAt: now,
264
+ updatedAt: now,
265
+ });
266
+ }
267
+ // 8. Crea operational data
268
+ await ctx.db.insert("operationalData", {
269
+ operationalDataId: 0,
270
+ prescriptionRef: prescriptionId,
271
+ });
272
+ // 9. Audit
273
+ await ctx.db.insert("auditEvents", {
274
+ auditEventId: 0,
275
+ prescriptionRef: prescriptionId,
276
+ at: now,
277
+ actorUserId: actor.actorUserId,
278
+ actorRole: actor.actorRole,
279
+ type: "PRESCRIPTION_CREATED",
280
+ payload: {
281
+ pdcItemId: args.pdcItemId,
282
+ flowKey: args.flowKey,
283
+ flowVersion: flow.version,
284
+ source: "createDraftFromBackend",
195
285
  },
196
286
  });
197
287
  return { prescriptionId, revisionId };
@@ -226,43 +316,17 @@ export const updateClinicalDraft = mutation({
226
316
  },
227
317
  handler: async (ctx, args) => {
228
318
  const now = Date.now();
229
- // 1. Auth identity + prescrizione
230
- let user;
231
- try {
232
- user = await requireIdentityUser(ctx);
233
- }
234
- catch (error) {
235
- if (error instanceof ConvexError) {
236
- throw error;
237
- }
238
- throw new ConvexError({
239
- code: "UNAUTHORIZED",
240
- message: "Accesso non autorizzato",
241
- });
242
- }
319
+ const identity = await requireIdentityOrThrow(ctx);
320
+ const actor = getActorFromIdentity(identity);
243
321
  const prescription = await ctx.db.get(args.prescriptionId);
244
322
  if (!prescription) {
245
323
  throw new Error("Prescrizione non trovata");
246
324
  }
247
- try {
248
- assertPrescriptionAccess(user, prescription);
249
- }
250
- catch (error) {
251
- throw new ConvexError({
252
- code: "FORBIDDEN",
253
- message: error instanceof Error ? error.message : "Forbidden",
254
- });
255
- }
256
325
  // 2. Verifica stato
257
326
  if (!["DRAFT", "PENDING_DOCTOR"].includes(prescription.status)) {
258
327
  throw new Error(`Non è possibile modificare una prescrizione in stato ${prescription.status}`);
259
328
  }
260
- // 3. Verifica permessi azione
261
- const permission = canUserPerformAction(user.role, "updateDraft", prescription.status);
262
- if (!permission.allowed) {
263
- throw new Error(permission.reason);
264
- }
265
- // 4. Carica revisione corrente
329
+ // 3. Carica revisione corrente
266
330
  if (!prescription.latestClinicalRevisionId) {
267
331
  throw new Error("Nessuna revisione clinica trovata");
268
332
  }
@@ -335,19 +399,20 @@ export const updateClinicalDraft = mutation({
335
399
  if (normalizedPatch.lineaMargine !== undefined) {
336
400
  changedKeys.push("lineaMargine");
337
401
  }
338
- // 10. Audit
402
+ // 9. Audit
339
403
  await ctx.db.insert("auditEvents", {
340
404
  auditEventId: 0, // Will be backfilled
341
405
  prescriptionRef: args.prescriptionId,
342
406
  at: now,
343
- actorUserId: user._id,
344
- actorRole: user.role,
345
- type: user.role === "DOCTOR" ? "DOCTOR_UPDATED_DRAFT" : "CLINICAL_DRAFT_UPDATED",
407
+ actorUserId: actor.actorUserId,
408
+ actorRole: actor.actorRole,
409
+ type: "CLINICAL_DRAFT_UPDATED",
346
410
  payload: {
347
411
  changes: normalizedPatch,
348
412
  changedKeys,
349
413
  clinicalValidationErrorsCount: clinicalValidationErrors.length,
350
414
  newHash,
415
+ actorEmail: identity.email,
351
416
  },
352
417
  });
353
418
  return { ok: true, success: true, clinicalHash: newHash, clinicalValidationErrors };
@@ -363,22 +428,18 @@ export const submitToDoctor = mutation({
363
428
  },
364
429
  handler: async (ctx, args) => {
365
430
  const now = Date.now();
366
- const user = await requireAuthenticatedUserOrThrow(ctx);
431
+ const identity = await requireIdentityOrThrow(ctx);
432
+ const actor = getActorFromIdentity(identity);
367
433
  // 1. Carica prescrizione
368
434
  const prescription = await ctx.db.get(args.prescriptionId);
369
435
  if (!prescription) {
370
436
  throw new Error("Prescrizione non trovata");
371
437
  }
372
- assertPrescriptionAccessOrThrow(user, prescription);
373
438
  // 2. Verifica stato
374
439
  if (prescription.status !== "DRAFT") {
375
440
  throw new Error(`Non è possibile inviare una prescrizione in stato ${prescription.status}`);
376
441
  }
377
- const permission = canUserPerformAction(user.role, "submit", prescription.status);
378
- if (!permission.allowed) {
379
- throw new Error(permission.reason);
380
- }
381
- // 4. Valida requisiti per prescrizioni digitali
442
+ // 3. Valida requisiti per prescrizioni digitali
382
443
  const digitalErrors = validateDigitalRequirements(prescription.prescriptionType, prescription.digitalAssets);
383
444
  if (digitalErrors.length > 0) {
384
445
  throw new Error(digitalErrors.map(e => e.message).join("; "));
@@ -388,15 +449,15 @@ export const submitToDoctor = mutation({
388
449
  status: "PENDING_DOCTOR",
389
450
  updatedAt: now,
390
451
  });
391
- // 6. Audit
452
+ // 5. Audit
392
453
  await ctx.db.insert("auditEvents", {
393
454
  auditEventId: 0, // Will be backfilled
394
455
  prescriptionRef: args.prescriptionId,
395
456
  at: now,
396
- actorUserId: user._id,
397
- actorRole: user.role,
457
+ actorUserId: actor.actorUserId,
458
+ actorRole: actor.actorRole,
398
459
  type: "SUBMITTED_TO_DOCTOR",
399
- payload: {},
460
+ payload: { actorEmail: identity.email },
400
461
  });
401
462
  return { success: true };
402
463
  },
@@ -412,24 +473,18 @@ export const signPrescription = mutation({
412
473
  },
413
474
  handler: async (ctx, args) => {
414
475
  const now = Date.now();
415
- const user = await requireAuthenticatedUserOrThrow(ctx);
476
+ const identity = await requireIdentityOrThrow(ctx);
477
+ const actor = getActorFromIdentity(identity);
416
478
  // 1. Carica prescrizione
417
479
  const prescription = await ctx.db.get(args.prescriptionId);
418
480
  if (!prescription) {
419
481
  throw new Error("Prescrizione non trovata");
420
482
  }
421
- assertPrescriptionAccessOrThrow(user, prescription);
422
483
  // 2. Verifica stato
423
484
  if (prescription.status !== "PENDING_DOCTOR") {
424
485
  throw new Error(`Non è possibile firmare una prescrizione in stato ${prescription.status}`);
425
486
  }
426
- if (user.role !== "DOCTOR") {
427
- throw new ConvexError({
428
- code: "FORBIDDEN",
429
- message: "Solo i medici possono firmare le prescrizioni",
430
- });
431
- }
432
- // 4. Carica flow e revisione
487
+ // 3. Carica flow e revisione
433
488
  const flow = await ctx.db
434
489
  .query("flows")
435
490
  .withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
@@ -460,7 +515,7 @@ export const signPrescription = mutation({
460
515
  // 7. Crea firma
461
516
  const signature = {
462
517
  signedAt: now,
463
- signedByDoctorId: user.coreDoctorId ?? user._id,
518
+ signedByDoctorId: prescription.doctorId,
464
519
  signatureType: "weak-digital",
465
520
  signaturePayload: args.signaturePayload,
466
521
  clinicalHash: finalHash,
@@ -515,18 +570,19 @@ export const signPrescription = mutation({
515
570
  status: "SIGNED",
516
571
  updatedAt: now,
517
572
  });
518
- // 11. Audit
573
+ // 10. Audit
519
574
  await ctx.db.insert("auditEvents", {
520
575
  auditEventId: 0, // Will be backfilled
521
576
  prescriptionRef: args.prescriptionId,
522
577
  at: now,
523
- actorUserId: user._id,
524
- actorRole: "DOCTOR",
578
+ actorUserId: actor.actorUserId,
579
+ actorRole: actor.actorRole,
525
580
  type: "PRESCRIPTION_SIGNED",
526
581
  payload: {
527
582
  clinicalHash: finalHash,
528
583
  signedAt: now,
529
584
  syncJobIds,
585
+ actorEmail: identity.email,
530
586
  },
531
587
  });
532
588
  return {
@@ -547,57 +603,17 @@ export const sendToLab = mutation({
547
603
  },
548
604
  handler: async (ctx, args) => {
549
605
  const now = Date.now();
550
- // 1. Auth identity + prescrizione
551
- let user;
552
- try {
553
- user = await requireIdentityUser(ctx);
554
- }
555
- catch (error) {
556
- if (error instanceof ConvexError) {
557
- throw error;
558
- }
559
- throw new ConvexError({
560
- code: "UNAUTHORIZED",
561
- message: "Accesso non autorizzato",
562
- });
563
- }
606
+ const identity = await requireIdentityOrThrow(ctx);
607
+ const actor = getActorFromIdentity(identity);
564
608
  const prescription = await ctx.db.get(args.prescriptionId);
565
609
  if (!prescription) {
566
610
  throw new Error("Prescrizione non trovata");
567
611
  }
568
- try {
569
- assertPrescriptionAccess(user, prescription);
570
- }
571
- catch (error) {
572
- throw new ConvexError({
573
- code: "FORBIDDEN",
574
- message: error instanceof Error ? error.message : "Forbidden",
575
- });
576
- }
577
612
  // 2. Verifica stato (deve essere DRAFT o PENDING_DOCTOR)
578
613
  if (prescription.status !== "DRAFT" && prescription.status !== "PENDING_DOCTOR") {
579
614
  throw new Error(`Non è possibile inviare al lab una prescrizione in stato ${prescription.status}`);
580
615
  }
581
- // 3. Policy invio vs policy auto-firma:
582
- // - invio consentito a ruoli autorizzati dal flusso corrente (submit/sign)
583
- // - auto-firma consentita solo al DOCTOR
584
- const canSubmit = canUserPerformAction(user.role, "submit", prescription.status).allowed;
585
- const canSign = canUserPerformAction(user.role, "sign", prescription.status).allowed;
586
- const canSendToLab = canSubmit || canSign;
587
- if (!canSendToLab) {
588
- throw new ConvexError({
589
- code: "FORBIDDEN",
590
- message: `Il ruolo ${user.role} non può inviare al laboratorio in stato ${prescription.status}`,
591
- });
592
- }
593
- const canAutoSign = user.role === "DOCTOR";
594
- if (!canAutoSign) {
595
- throw new ConvexError({
596
- code: "FORBIDDEN",
597
- message: "Invio al laboratorio non consentito: auto-firma disponibile solo per DOCTOR",
598
- });
599
- }
600
- // 4. Carica flow e revisione
616
+ // 3. Carica flow e revisione
601
617
  const flow = await ctx.db
602
618
  .query("flows")
603
619
  .withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
@@ -633,8 +649,8 @@ export const sendToLab = mutation({
633
649
  signaturePayload: btoa(JSON.stringify({
634
650
  autoSigned: true,
635
651
  timestamp: now,
636
- sentBy: user._id,
637
- sentByRole: user.role,
652
+ sentBy: actor.actorUserId,
653
+ sentByRole: actor.actorRole,
638
654
  })),
639
655
  clinicalHash: finalHash,
640
656
  };
@@ -693,14 +709,15 @@ export const sendToLab = mutation({
693
709
  auditEventId: 0, // Will be backfilled
694
710
  prescriptionRef: args.prescriptionId,
695
711
  at: now,
696
- actorUserId: user._id,
697
- actorRole: user.role,
712
+ actorUserId: actor.actorUserId,
713
+ actorRole: actor.actorRole,
698
714
  type: "SENT_TO_LAB",
699
715
  payload: {
700
716
  clinicalHash: finalHash,
701
717
  sentAt: now,
702
718
  syncJobIds,
703
719
  autoSigned: true,
720
+ actorEmail: identity.email,
704
721
  },
705
722
  });
706
723
  return {
@@ -721,24 +738,18 @@ export const reviseAfterSignature = mutation({
721
738
  },
722
739
  handler: async (ctx, args) => {
723
740
  const now = Date.now();
724
- const user = await requireAuthenticatedUserOrThrow(ctx);
741
+ const identity = await requireIdentityOrThrow(ctx);
742
+ const actor = getActorFromIdentity(identity);
725
743
  // 1. Carica prescrizione
726
744
  const prescription = await ctx.db.get(args.prescriptionId);
727
745
  if (!prescription) {
728
746
  throw new Error("Prescrizione non trovata");
729
747
  }
730
- assertPrescriptionAccessOrThrow(user, prescription);
731
748
  // 2. Verifica stato
732
749
  if (!["SIGNED", "ERROR"].includes(prescription.status)) {
733
750
  throw new Error(`Non è possibile revisionare una prescrizione in stato ${prescription.status}`);
734
751
  }
735
- if (user.role !== "DOCTOR") {
736
- throw new ConvexError({
737
- code: "FORBIDDEN",
738
- message: "Solo i medici possono creare revisioni",
739
- });
740
- }
741
- // 4. Carica revisione corrente
752
+ // 3. Carica revisione corrente
742
753
  if (!prescription.latestClinicalRevisionId) {
743
754
  throw new Error("Nessuna revisione clinica trovata");
744
755
  }
@@ -770,14 +781,15 @@ export const reviseAfterSignature = mutation({
770
781
  auditEventId: 0, // Will be backfilled
771
782
  prescriptionRef: args.prescriptionId,
772
783
  at: now,
773
- actorUserId: user._id,
774
- actorRole: "DOCTOR",
784
+ actorUserId: actor.actorUserId,
785
+ actorRole: actor.actorRole,
775
786
  type: "REVISION_CREATED",
776
787
  payload: {
777
788
  previousRevisionNumber: currentRevision.revisionNumber,
778
789
  newRevisionNumber,
779
790
  previousHash: currentRevision.clinicalHash,
780
791
  reason: args.reason,
792
+ actorEmail: identity.email,
781
793
  },
782
794
  });
783
795
  return {
@@ -797,13 +809,13 @@ export const cancelPrescription = mutation({
797
809
  },
798
810
  handler: async (ctx, args) => {
799
811
  const now = Date.now();
800
- const user = await requireAuthenticatedUserOrThrow(ctx);
812
+ const identity = await requireIdentityOrThrow(ctx);
813
+ const actor = getActorFromIdentity(identity);
801
814
  // 1. Carica prescrizione
802
815
  const prescription = await ctx.db.get(args.prescriptionId);
803
816
  if (!prescription) {
804
817
  throw new Error("Prescrizione non trovata");
805
818
  }
806
- assertPrescriptionAccessOrThrow(user, prescription);
807
819
  // 2. Verifica stato (non può annullare se già SYNCED)
808
820
  if (prescription.status === "SYNCED") {
809
821
  throw new Error("Non è possibile annullare una prescrizione già sincronizzata");
@@ -811,24 +823,20 @@ export const cancelPrescription = mutation({
811
823
  if (prescription.status === "CANCELLED") {
812
824
  throw new Error("La prescrizione è già annullata");
813
825
  }
814
- const permission = canUserPerformAction(user.role, "cancel", prescription.status);
815
- if (!permission.allowed) {
816
- throw new Error(permission.reason);
817
- }
818
- // 4. Aggiorna stato
826
+ // 3. Aggiorna stato
819
827
  await ctx.db.patch(args.prescriptionId, {
820
828
  status: "CANCELLED",
821
829
  updatedAt: now,
822
830
  });
823
- // 5. Audit
831
+ // 4. Audit
824
832
  await ctx.db.insert("auditEvents", {
825
833
  auditEventId: 0, // Will be backfilled
826
834
  prescriptionRef: args.prescriptionId,
827
835
  at: now,
828
- actorUserId: user._id,
829
- actorRole: user.role,
836
+ actorUserId: actor.actorUserId,
837
+ actorRole: actor.actorRole,
830
838
  type: "PRESCRIPTION_CANCELLED",
831
- payload: { reason: args.reason },
839
+ payload: { reason: args.reason, actorEmail: identity.email },
832
840
  });
833
841
  return { success: true };
834
842
  },
@@ -844,13 +852,13 @@ export const markAsShipped = mutation({
844
852
  },
845
853
  handler: async (ctx, args) => {
846
854
  const now = Date.now();
847
- const user = await requireAuthenticatedUserOrThrow(ctx);
855
+ const identity = await requireIdentityOrThrow(ctx);
856
+ const actor = getActorFromIdentity(identity);
848
857
  // 1. Carica prescrizione
849
858
  const prescription = await ctx.db.get(args.prescriptionId);
850
859
  if (!prescription) {
851
860
  throw new Error("Prescrizione non trovata");
852
861
  }
853
- assertPrescriptionAccessOrThrow(user, prescription);
854
862
  // 2. Verifica stato (deve essere SIGNED o superiore, non CANCELLED)
855
863
  const validStatuses = ["SIGNED", "SYNCED", "ERROR"];
856
864
  if (!validStatuses.includes(prescription.status)) {
@@ -860,35 +868,25 @@ export const markAsShipped = mutation({
860
868
  if (prescription.shippedAt) {
861
869
  throw new Error("Questa prescrizione è già stata marcata come spedita");
862
870
  }
863
- // 4. Policy ruolo: segreteria/medico e admin builder possono marcare spedito
864
- const canManageShipment = user.role === "SECRETARY" ||
865
- user.role === "DOCTOR" ||
866
- user.adminRole === "admin" ||
867
- user.adminRole === "superadmin";
868
- if (!canManageShipment) {
869
- throw new ConvexError({
870
- code: "FORBIDDEN",
871
- message: "Ruolo non autorizzato alla gestione spedizione",
872
- });
873
- }
874
- // 5. Aggiorna prescrizione
871
+ // 3. Aggiorna prescrizione
875
872
  await ctx.db.patch(args.prescriptionId, {
876
873
  shippedAt: now,
877
- shippedBy: user._id,
874
+ shippedBy: actor.actorUserId,
878
875
  trackingNumber: args.trackingNumber,
879
876
  updatedAt: now,
880
877
  });
881
- // 6. Audit
878
+ // 4. Audit
882
879
  await ctx.db.insert("auditEvents", {
883
880
  auditEventId: 0, // Will be backfilled
884
881
  prescriptionRef: args.prescriptionId,
885
882
  at: now,
886
- actorUserId: user._id,
887
- actorRole: user.role,
883
+ actorUserId: actor.actorUserId,
884
+ actorRole: actor.actorRole,
888
885
  type: "PRESCRIPTION_SHIPPED",
889
886
  payload: {
890
887
  trackingNumber: args.trackingNumber,
891
888
  shippedAt: now,
889
+ actorEmail: identity.email,
892
890
  },
893
891
  });
894
892
  return { success: true, shippedAt: now };
@@ -903,44 +901,33 @@ export const unmarkAsShipped = mutation({
903
901
  },
904
902
  handler: async (ctx, args) => {
905
903
  const now = Date.now();
906
- const user = await requireAuthenticatedUserOrThrow(ctx);
904
+ const identity = await requireIdentityOrThrow(ctx);
905
+ const actor = getActorFromIdentity(identity);
907
906
  // 1. Carica prescrizione
908
907
  const prescription = await ctx.db.get(args.prescriptionId);
909
908
  if (!prescription) {
910
909
  throw new Error("Prescrizione non trovata");
911
910
  }
912
- assertPrescriptionAccessOrThrow(user, prescription);
913
911
  // 2. Verifica se è spedita
914
912
  if (!prescription.shippedAt) {
915
913
  throw new Error("Questa prescrizione non è marcata come spedita");
916
914
  }
917
- // 3. Policy ruolo: segreteria/medico e admin builder possono annullare spedizione
918
- const canManageShipment = user.role === "SECRETARY" ||
919
- user.role === "DOCTOR" ||
920
- user.adminRole === "admin" ||
921
- user.adminRole === "superadmin";
922
- if (!canManageShipment) {
923
- throw new ConvexError({
924
- code: "FORBIDDEN",
925
- message: "Ruolo non autorizzato alla gestione spedizione",
926
- });
927
- }
928
- // 4. Aggiorna prescrizione
915
+ // 3. Aggiorna prescrizione
929
916
  await ctx.db.patch(args.prescriptionId, {
930
917
  shippedAt: undefined,
931
918
  shippedBy: undefined,
932
919
  trackingNumber: undefined,
933
920
  updatedAt: now,
934
921
  });
935
- // 5. Audit
922
+ // 4. Audit
936
923
  await ctx.db.insert("auditEvents", {
937
924
  auditEventId: 0, // Will be backfilled
938
925
  prescriptionRef: args.prescriptionId,
939
926
  at: now,
940
- actorUserId: user._id,
941
- actorRole: user.role,
927
+ actorUserId: actor.actorUserId,
928
+ actorRole: actor.actorRole,
942
929
  type: "PRESCRIPTION_SHIPMENT_CANCELLED",
943
- payload: {},
930
+ payload: { actorEmail: identity.email },
944
931
  });
945
932
  return { success: true };
946
933
  },
@@ -979,22 +966,8 @@ export const createFromCalendar = mutation({
979
966
  handler: async (ctx, args) => {
980
967
  const now = Date.now();
981
968
  const flowKey = args.flowKey ?? "prosthetics-standard";
982
- const user = await requireAuthenticatedUserOrThrow(ctx);
983
- const canCreate = user.role === "SECRETARY" ||
984
- user.adminRole === "admin" ||
985
- user.adminRole === "superadmin";
986
- if (!canCreate) {
987
- throw new ConvexError({
988
- code: "FORBIDDEN",
989
- message: "Utente non autorizzato a creare prescrizioni",
990
- });
991
- }
992
- if (user.clinicId !== args.clinicId && user.adminRole !== "admin" && user.adminRole !== "superadmin") {
993
- throw new ConvexError({
994
- code: "FORBIDDEN",
995
- message: "Clinic non autorizzata per l'utente autenticato",
996
- });
997
- }
969
+ const identity = await requireIdentityOrThrow(ctx);
970
+ const actor = getActorFromIdentity(identity);
998
971
  // =============================================
999
972
  // 1. CONTROLLO IDEMPOTENZA
1000
973
  // Se esiste già una prescrizione con questa idempotencyKey,
@@ -1119,7 +1092,7 @@ export const createFromCalendar = mutation({
1119
1092
  flowKey: actualFlowKey, // Usa il flow effettivo (potrebbe essere il default)
1120
1093
  flowVersion: flow.version,
1121
1094
  status: "DRAFT",
1122
- createdByUserId: user._id, // Derivato dall'identità autenticata
1095
+ createdByUserId: actor.actorUserId, // Derivato dall'identità autenticata
1123
1096
  createdAt: now,
1124
1097
  updatedAt: now,
1125
1098
  coreRefs: {
@@ -1232,8 +1205,8 @@ export const createFromCalendar = mutation({
1232
1205
  auditEventId: 0, // Will be backfilled
1233
1206
  prescriptionRef: prescriptionId,
1234
1207
  at: now,
1235
- actorUserId: user._id,
1236
- actorRole: "SYSTEM", // Creato automaticamente dal sistema
1208
+ actorUserId: actor.actorUserId,
1209
+ actorRole: actor.actorRole,
1237
1210
  type: "PRESCRIPTION_CREATED_FROM_CALENDAR",
1238
1211
  payload: {
1239
1212
  idempotencyKey: args.idempotencyKey,
@@ -1245,6 +1218,7 @@ export const createFromCalendar = mutation({
1245
1218
  patientName: `${args.calendarData.patientFirstName} ${args.calendarData.patientLastName}`,
1246
1219
  treatmentName: args.calendarData.treatmentName,
1247
1220
  phaseName: args.calendarData.phaseName,
1221
+ actorEmail: identity.email,
1248
1222
  },
1249
1223
  });
1250
1224
  // =============================================