@primocaredentgroup/prescriptions-component 0.1.4 → 0.1.6

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 (33) hide show
  1. package/README.md +16 -0
  2. package/dist/convex/lib/auth.d.ts +5 -0
  3. package/dist/convex/lib/auth.d.ts.map +1 -1
  4. package/dist/convex/lib/auth.js +36 -0
  5. package/dist/convex/lib/auth.js.map +1 -1
  6. package/dist/convex/mutations/digitalAssets.d.ts.map +1 -1
  7. package/dist/convex/mutations/digitalAssets.js +20 -61
  8. package/dist/convex/mutations/digitalAssets.js.map +1 -1
  9. package/dist/convex/mutations/operational.d.ts.map +1 -1
  10. package/dist/convex/mutations/operational.js +19 -54
  11. package/dist/convex/mutations/operational.js.map +1 -1
  12. package/dist/convex/mutations/phases.d.ts.map +1 -1
  13. package/dist/convex/mutations/phases.js +20 -52
  14. package/dist/convex/mutations/phases.js.map +1 -1
  15. package/dist/convex/mutations/prescriptions.d.ts.map +1 -1
  16. package/dist/convex/mutations/prescriptions.js +80 -229
  17. package/dist/convex/mutations/prescriptions.js.map +1 -1
  18. package/dist/convex/mutations/syncJobs.d.ts.map +1 -1
  19. package/dist/convex/mutations/syncJobs.js +6 -16
  20. package/dist/convex/mutations/syncJobs.js.map +1 -1
  21. package/dist/convex/prescriptions/fields.d.ts.map +1 -1
  22. package/dist/convex/prescriptions/fields.js +6 -7
  23. package/dist/convex/prescriptions/fields.js.map +1 -1
  24. package/dist/convex/queries/dynamicFields.d.ts.map +1 -1
  25. package/dist/convex/queries/dynamicFields.js +2 -3
  26. package/dist/convex/queries/dynamicFields.js.map +1 -1
  27. package/dist/convex/schema.d.ts +2 -2
  28. package/dist/convex/schema.d.ts.map +1 -1
  29. package/dist/convex/schema.js +2 -2
  30. package/dist/convex/schema.js.map +1 -1
  31. package/dist/convex/types.d.ts +1 -1
  32. package/dist/convex/types.d.ts.map +1 -1
  33. package/package.json +9 -9
@@ -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,19 @@ 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,
195
162
  },
196
163
  });
197
164
  return { prescriptionId, revisionId };
@@ -226,43 +193,17 @@ export const updateClinicalDraft = mutation({
226
193
  },
227
194
  handler: async (ctx, args) => {
228
195
  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
- }
196
+ const identity = await requireIdentityOrThrow(ctx);
197
+ const actor = getActorFromIdentity(identity);
243
198
  const prescription = await ctx.db.get(args.prescriptionId);
244
199
  if (!prescription) {
245
200
  throw new Error("Prescrizione non trovata");
246
201
  }
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
202
  // 2. Verifica stato
257
203
  if (!["DRAFT", "PENDING_DOCTOR"].includes(prescription.status)) {
258
204
  throw new Error(`Non è possibile modificare una prescrizione in stato ${prescription.status}`);
259
205
  }
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
206
+ // 3. Carica revisione corrente
266
207
  if (!prescription.latestClinicalRevisionId) {
267
208
  throw new Error("Nessuna revisione clinica trovata");
268
209
  }
@@ -335,19 +276,20 @@ export const updateClinicalDraft = mutation({
335
276
  if (normalizedPatch.lineaMargine !== undefined) {
336
277
  changedKeys.push("lineaMargine");
337
278
  }
338
- // 10. Audit
279
+ // 9. Audit
339
280
  await ctx.db.insert("auditEvents", {
340
281
  auditEventId: 0, // Will be backfilled
341
282
  prescriptionRef: args.prescriptionId,
342
283
  at: now,
343
- actorUserId: user._id,
344
- actorRole: user.role,
345
- type: user.role === "DOCTOR" ? "DOCTOR_UPDATED_DRAFT" : "CLINICAL_DRAFT_UPDATED",
284
+ actorUserId: actor.actorUserId,
285
+ actorRole: actor.actorRole,
286
+ type: "CLINICAL_DRAFT_UPDATED",
346
287
  payload: {
347
288
  changes: normalizedPatch,
348
289
  changedKeys,
349
290
  clinicalValidationErrorsCount: clinicalValidationErrors.length,
350
291
  newHash,
292
+ actorEmail: identity.email,
351
293
  },
352
294
  });
353
295
  return { ok: true, success: true, clinicalHash: newHash, clinicalValidationErrors };
@@ -363,22 +305,18 @@ export const submitToDoctor = mutation({
363
305
  },
364
306
  handler: async (ctx, args) => {
365
307
  const now = Date.now();
366
- const user = await requireAuthenticatedUserOrThrow(ctx);
308
+ const identity = await requireIdentityOrThrow(ctx);
309
+ const actor = getActorFromIdentity(identity);
367
310
  // 1. Carica prescrizione
368
311
  const prescription = await ctx.db.get(args.prescriptionId);
369
312
  if (!prescription) {
370
313
  throw new Error("Prescrizione non trovata");
371
314
  }
372
- assertPrescriptionAccessOrThrow(user, prescription);
373
315
  // 2. Verifica stato
374
316
  if (prescription.status !== "DRAFT") {
375
317
  throw new Error(`Non è possibile inviare una prescrizione in stato ${prescription.status}`);
376
318
  }
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
319
+ // 3. Valida requisiti per prescrizioni digitali
382
320
  const digitalErrors = validateDigitalRequirements(prescription.prescriptionType, prescription.digitalAssets);
383
321
  if (digitalErrors.length > 0) {
384
322
  throw new Error(digitalErrors.map(e => e.message).join("; "));
@@ -388,15 +326,15 @@ export const submitToDoctor = mutation({
388
326
  status: "PENDING_DOCTOR",
389
327
  updatedAt: now,
390
328
  });
391
- // 6. Audit
329
+ // 5. Audit
392
330
  await ctx.db.insert("auditEvents", {
393
331
  auditEventId: 0, // Will be backfilled
394
332
  prescriptionRef: args.prescriptionId,
395
333
  at: now,
396
- actorUserId: user._id,
397
- actorRole: user.role,
334
+ actorUserId: actor.actorUserId,
335
+ actorRole: actor.actorRole,
398
336
  type: "SUBMITTED_TO_DOCTOR",
399
- payload: {},
337
+ payload: { actorEmail: identity.email },
400
338
  });
401
339
  return { success: true };
402
340
  },
@@ -412,24 +350,18 @@ export const signPrescription = mutation({
412
350
  },
413
351
  handler: async (ctx, args) => {
414
352
  const now = Date.now();
415
- const user = await requireAuthenticatedUserOrThrow(ctx);
353
+ const identity = await requireIdentityOrThrow(ctx);
354
+ const actor = getActorFromIdentity(identity);
416
355
  // 1. Carica prescrizione
417
356
  const prescription = await ctx.db.get(args.prescriptionId);
418
357
  if (!prescription) {
419
358
  throw new Error("Prescrizione non trovata");
420
359
  }
421
- assertPrescriptionAccessOrThrow(user, prescription);
422
360
  // 2. Verifica stato
423
361
  if (prescription.status !== "PENDING_DOCTOR") {
424
362
  throw new Error(`Non è possibile firmare una prescrizione in stato ${prescription.status}`);
425
363
  }
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
364
+ // 3. Carica flow e revisione
433
365
  const flow = await ctx.db
434
366
  .query("flows")
435
367
  .withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
@@ -460,7 +392,7 @@ export const signPrescription = mutation({
460
392
  // 7. Crea firma
461
393
  const signature = {
462
394
  signedAt: now,
463
- signedByDoctorId: user.coreDoctorId ?? user._id,
395
+ signedByDoctorId: prescription.doctorId,
464
396
  signatureType: "weak-digital",
465
397
  signaturePayload: args.signaturePayload,
466
398
  clinicalHash: finalHash,
@@ -515,18 +447,19 @@ export const signPrescription = mutation({
515
447
  status: "SIGNED",
516
448
  updatedAt: now,
517
449
  });
518
- // 11. Audit
450
+ // 10. Audit
519
451
  await ctx.db.insert("auditEvents", {
520
452
  auditEventId: 0, // Will be backfilled
521
453
  prescriptionRef: args.prescriptionId,
522
454
  at: now,
523
- actorUserId: user._id,
524
- actorRole: "DOCTOR",
455
+ actorUserId: actor.actorUserId,
456
+ actorRole: actor.actorRole,
525
457
  type: "PRESCRIPTION_SIGNED",
526
458
  payload: {
527
459
  clinicalHash: finalHash,
528
460
  signedAt: now,
529
461
  syncJobIds,
462
+ actorEmail: identity.email,
530
463
  },
531
464
  });
532
465
  return {
@@ -547,57 +480,17 @@ export const sendToLab = mutation({
547
480
  },
548
481
  handler: async (ctx, args) => {
549
482
  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
- }
483
+ const identity = await requireIdentityOrThrow(ctx);
484
+ const actor = getActorFromIdentity(identity);
564
485
  const prescription = await ctx.db.get(args.prescriptionId);
565
486
  if (!prescription) {
566
487
  throw new Error("Prescrizione non trovata");
567
488
  }
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
489
  // 2. Verifica stato (deve essere DRAFT o PENDING_DOCTOR)
578
490
  if (prescription.status !== "DRAFT" && prescription.status !== "PENDING_DOCTOR") {
579
491
  throw new Error(`Non è possibile inviare al lab una prescrizione in stato ${prescription.status}`);
580
492
  }
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
493
+ // 3. Carica flow e revisione
601
494
  const flow = await ctx.db
602
495
  .query("flows")
603
496
  .withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
@@ -633,8 +526,8 @@ export const sendToLab = mutation({
633
526
  signaturePayload: btoa(JSON.stringify({
634
527
  autoSigned: true,
635
528
  timestamp: now,
636
- sentBy: user._id,
637
- sentByRole: user.role,
529
+ sentBy: actor.actorUserId,
530
+ sentByRole: actor.actorRole,
638
531
  })),
639
532
  clinicalHash: finalHash,
640
533
  };
@@ -693,14 +586,15 @@ export const sendToLab = mutation({
693
586
  auditEventId: 0, // Will be backfilled
694
587
  prescriptionRef: args.prescriptionId,
695
588
  at: now,
696
- actorUserId: user._id,
697
- actorRole: user.role,
589
+ actorUserId: actor.actorUserId,
590
+ actorRole: actor.actorRole,
698
591
  type: "SENT_TO_LAB",
699
592
  payload: {
700
593
  clinicalHash: finalHash,
701
594
  sentAt: now,
702
595
  syncJobIds,
703
596
  autoSigned: true,
597
+ actorEmail: identity.email,
704
598
  },
705
599
  });
706
600
  return {
@@ -721,24 +615,18 @@ export const reviseAfterSignature = mutation({
721
615
  },
722
616
  handler: async (ctx, args) => {
723
617
  const now = Date.now();
724
- const user = await requireAuthenticatedUserOrThrow(ctx);
618
+ const identity = await requireIdentityOrThrow(ctx);
619
+ const actor = getActorFromIdentity(identity);
725
620
  // 1. Carica prescrizione
726
621
  const prescription = await ctx.db.get(args.prescriptionId);
727
622
  if (!prescription) {
728
623
  throw new Error("Prescrizione non trovata");
729
624
  }
730
- assertPrescriptionAccessOrThrow(user, prescription);
731
625
  // 2. Verifica stato
732
626
  if (!["SIGNED", "ERROR"].includes(prescription.status)) {
733
627
  throw new Error(`Non è possibile revisionare una prescrizione in stato ${prescription.status}`);
734
628
  }
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
629
+ // 3. Carica revisione corrente
742
630
  if (!prescription.latestClinicalRevisionId) {
743
631
  throw new Error("Nessuna revisione clinica trovata");
744
632
  }
@@ -770,14 +658,15 @@ export const reviseAfterSignature = mutation({
770
658
  auditEventId: 0, // Will be backfilled
771
659
  prescriptionRef: args.prescriptionId,
772
660
  at: now,
773
- actorUserId: user._id,
774
- actorRole: "DOCTOR",
661
+ actorUserId: actor.actorUserId,
662
+ actorRole: actor.actorRole,
775
663
  type: "REVISION_CREATED",
776
664
  payload: {
777
665
  previousRevisionNumber: currentRevision.revisionNumber,
778
666
  newRevisionNumber,
779
667
  previousHash: currentRevision.clinicalHash,
780
668
  reason: args.reason,
669
+ actorEmail: identity.email,
781
670
  },
782
671
  });
783
672
  return {
@@ -797,13 +686,13 @@ export const cancelPrescription = mutation({
797
686
  },
798
687
  handler: async (ctx, args) => {
799
688
  const now = Date.now();
800
- const user = await requireAuthenticatedUserOrThrow(ctx);
689
+ const identity = await requireIdentityOrThrow(ctx);
690
+ const actor = getActorFromIdentity(identity);
801
691
  // 1. Carica prescrizione
802
692
  const prescription = await ctx.db.get(args.prescriptionId);
803
693
  if (!prescription) {
804
694
  throw new Error("Prescrizione non trovata");
805
695
  }
806
- assertPrescriptionAccessOrThrow(user, prescription);
807
696
  // 2. Verifica stato (non può annullare se già SYNCED)
808
697
  if (prescription.status === "SYNCED") {
809
698
  throw new Error("Non è possibile annullare una prescrizione già sincronizzata");
@@ -811,24 +700,20 @@ export const cancelPrescription = mutation({
811
700
  if (prescription.status === "CANCELLED") {
812
701
  throw new Error("La prescrizione è già annullata");
813
702
  }
814
- const permission = canUserPerformAction(user.role, "cancel", prescription.status);
815
- if (!permission.allowed) {
816
- throw new Error(permission.reason);
817
- }
818
- // 4. Aggiorna stato
703
+ // 3. Aggiorna stato
819
704
  await ctx.db.patch(args.prescriptionId, {
820
705
  status: "CANCELLED",
821
706
  updatedAt: now,
822
707
  });
823
- // 5. Audit
708
+ // 4. Audit
824
709
  await ctx.db.insert("auditEvents", {
825
710
  auditEventId: 0, // Will be backfilled
826
711
  prescriptionRef: args.prescriptionId,
827
712
  at: now,
828
- actorUserId: user._id,
829
- actorRole: user.role,
713
+ actorUserId: actor.actorUserId,
714
+ actorRole: actor.actorRole,
830
715
  type: "PRESCRIPTION_CANCELLED",
831
- payload: { reason: args.reason },
716
+ payload: { reason: args.reason, actorEmail: identity.email },
832
717
  });
833
718
  return { success: true };
834
719
  },
@@ -844,13 +729,13 @@ export const markAsShipped = mutation({
844
729
  },
845
730
  handler: async (ctx, args) => {
846
731
  const now = Date.now();
847
- const user = await requireAuthenticatedUserOrThrow(ctx);
732
+ const identity = await requireIdentityOrThrow(ctx);
733
+ const actor = getActorFromIdentity(identity);
848
734
  // 1. Carica prescrizione
849
735
  const prescription = await ctx.db.get(args.prescriptionId);
850
736
  if (!prescription) {
851
737
  throw new Error("Prescrizione non trovata");
852
738
  }
853
- assertPrescriptionAccessOrThrow(user, prescription);
854
739
  // 2. Verifica stato (deve essere SIGNED o superiore, non CANCELLED)
855
740
  const validStatuses = ["SIGNED", "SYNCED", "ERROR"];
856
741
  if (!validStatuses.includes(prescription.status)) {
@@ -860,35 +745,25 @@ export const markAsShipped = mutation({
860
745
  if (prescription.shippedAt) {
861
746
  throw new Error("Questa prescrizione è già stata marcata come spedita");
862
747
  }
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
748
+ // 3. Aggiorna prescrizione
875
749
  await ctx.db.patch(args.prescriptionId, {
876
750
  shippedAt: now,
877
- shippedBy: user._id,
751
+ shippedBy: actor.actorUserId,
878
752
  trackingNumber: args.trackingNumber,
879
753
  updatedAt: now,
880
754
  });
881
- // 6. Audit
755
+ // 4. Audit
882
756
  await ctx.db.insert("auditEvents", {
883
757
  auditEventId: 0, // Will be backfilled
884
758
  prescriptionRef: args.prescriptionId,
885
759
  at: now,
886
- actorUserId: user._id,
887
- actorRole: user.role,
760
+ actorUserId: actor.actorUserId,
761
+ actorRole: actor.actorRole,
888
762
  type: "PRESCRIPTION_SHIPPED",
889
763
  payload: {
890
764
  trackingNumber: args.trackingNumber,
891
765
  shippedAt: now,
766
+ actorEmail: identity.email,
892
767
  },
893
768
  });
894
769
  return { success: true, shippedAt: now };
@@ -903,44 +778,33 @@ export const unmarkAsShipped = mutation({
903
778
  },
904
779
  handler: async (ctx, args) => {
905
780
  const now = Date.now();
906
- const user = await requireAuthenticatedUserOrThrow(ctx);
781
+ const identity = await requireIdentityOrThrow(ctx);
782
+ const actor = getActorFromIdentity(identity);
907
783
  // 1. Carica prescrizione
908
784
  const prescription = await ctx.db.get(args.prescriptionId);
909
785
  if (!prescription) {
910
786
  throw new Error("Prescrizione non trovata");
911
787
  }
912
- assertPrescriptionAccessOrThrow(user, prescription);
913
788
  // 2. Verifica se è spedita
914
789
  if (!prescription.shippedAt) {
915
790
  throw new Error("Questa prescrizione non è marcata come spedita");
916
791
  }
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
792
+ // 3. Aggiorna prescrizione
929
793
  await ctx.db.patch(args.prescriptionId, {
930
794
  shippedAt: undefined,
931
795
  shippedBy: undefined,
932
796
  trackingNumber: undefined,
933
797
  updatedAt: now,
934
798
  });
935
- // 5. Audit
799
+ // 4. Audit
936
800
  await ctx.db.insert("auditEvents", {
937
801
  auditEventId: 0, // Will be backfilled
938
802
  prescriptionRef: args.prescriptionId,
939
803
  at: now,
940
- actorUserId: user._id,
941
- actorRole: user.role,
804
+ actorUserId: actor.actorUserId,
805
+ actorRole: actor.actorRole,
942
806
  type: "PRESCRIPTION_SHIPMENT_CANCELLED",
943
- payload: {},
807
+ payload: { actorEmail: identity.email },
944
808
  });
945
809
  return { success: true };
946
810
  },
@@ -979,22 +843,8 @@ export const createFromCalendar = mutation({
979
843
  handler: async (ctx, args) => {
980
844
  const now = Date.now();
981
845
  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
- }
846
+ const identity = await requireIdentityOrThrow(ctx);
847
+ const actor = getActorFromIdentity(identity);
998
848
  // =============================================
999
849
  // 1. CONTROLLO IDEMPOTENZA
1000
850
  // Se esiste già una prescrizione con questa idempotencyKey,
@@ -1119,7 +969,7 @@ export const createFromCalendar = mutation({
1119
969
  flowKey: actualFlowKey, // Usa il flow effettivo (potrebbe essere il default)
1120
970
  flowVersion: flow.version,
1121
971
  status: "DRAFT",
1122
- createdByUserId: user._id, // Derivato dall'identità autenticata
972
+ createdByUserId: actor.actorUserId, // Derivato dall'identità autenticata
1123
973
  createdAt: now,
1124
974
  updatedAt: now,
1125
975
  coreRefs: {
@@ -1232,8 +1082,8 @@ export const createFromCalendar = mutation({
1232
1082
  auditEventId: 0, // Will be backfilled
1233
1083
  prescriptionRef: prescriptionId,
1234
1084
  at: now,
1235
- actorUserId: user._id,
1236
- actorRole: "SYSTEM", // Creato automaticamente dal sistema
1085
+ actorUserId: actor.actorUserId,
1086
+ actorRole: actor.actorRole,
1237
1087
  type: "PRESCRIPTION_CREATED_FROM_CALENDAR",
1238
1088
  payload: {
1239
1089
  idempotencyKey: args.idempotencyKey,
@@ -1245,6 +1095,7 @@ export const createFromCalendar = mutation({
1245
1095
  patientName: `${args.calendarData.patientFirstName} ${args.calendarData.patientLastName}`,
1246
1096
  treatmentName: args.calendarData.treatmentName,
1247
1097
  phaseName: args.calendarData.phaseName,
1098
+ actorEmail: identity.email,
1248
1099
  },
1249
1100
  });
1250
1101
  // =============================================