@@ -31,6 +31,11 @@ import {
3131} from '../../common/entities/audit-log.entity' ;
3232import { StorageService } from '../storage/storage.service' ;
3333import { JobQueueService } from '../job-queue/job-queue.service' ;
34+ import {
35+ StorageQuotaService ,
36+ QuotaReservation ,
37+ } from '../storage-quota/storage-quota.service' ;
38+ import { QuotaUploadKind } from '../storage-quota/entities/storage-quota-ledger.entity' ;
3439
3540const ALLOWED_TRANSITIONS : Record < DisputeStatus , DisputeStatus [ ] > = {
3641 [ DisputeStatus . OPEN ] : [
@@ -87,6 +92,7 @@ export class DisputesService {
8792 private readonly auditLogService : AuditLogService ,
8893 private readonly storageService : StorageService ,
8994 private readonly jobQueueService : JobQueueService ,
95+ private readonly quotaService : StorageQuotaService ,
9096 ) { }
9197
9298 async createDispute (
@@ -392,12 +398,43 @@ export class DisputesService {
392398 dto : UploadEvidenceDto ,
393399 ) : Promise < DisputeEvidence > {
394400 // Verify dispute exists
395- await this . findOne ( disputeId ) ;
401+ const dispute = await this . findOne ( disputeId ) ;
402+
403+ // Reserve quota BEFORE writing to storage. Attribution goes to the
404+ // disputer only — `dto.uploadedBy` is a freeform staff label (e.g.
405+ // "Hospital Admin") and must NOT be used as the quota userId, since
406+ // it is not a UUID and would collide with other staff uploads for
407+ // the same dispute on the (userId, tenantId) UNIQUE index.
408+ const attributedUserId = dispute . disputedBy ;
409+ if ( ! attributedUserId ) {
410+ throw new BadRequestException (
411+ 'Cannot attribute upload quota: dispute has no attributed disputer' ,
412+ ) ;
413+ }
414+ await this . quotaService . ensureQuotaRow ( attributedUserId , 'verified' ) ;
415+
416+ const reservation : QuotaReservation = await this . quotaService . reserve (
417+ attributedUserId ,
418+ file . size ,
419+ {
420+ uploadKind : QuotaUploadKind . DISPUTE_EVIDENCE ,
421+ reason : 'dispute-evidence' ,
422+ } ,
423+ ) ;
396424
397- // Persist file to storage
398- const storagePath = await this . storageService . saveFile ( file ) ;
425+ // Write file. If this fails we MUST release the reservation.
426+ let storagePath : string ;
427+ try {
428+ storagePath = await this . storageService . saveFile ( file ) ;
429+ } catch ( err ) {
430+ await this . quotaService . release ( reservation . reservationId , {
431+ reason : 'storage-write-failed' ,
432+ } ) ;
433+ throw err ;
434+ }
399435
400- // Create evidence record with PENDING status
436+ // Create evidence record with PENDING status and the reservation token
437+ // so the dispute-evidence processor can reconcile on completion.
401438 const evidence = this . evidenceRepository . create ( {
402439 disputeId,
403440 originalFilename : file . originalname ,
@@ -407,18 +444,27 @@ export class DisputesService {
407444 uploadedBy : dto . uploadedBy ,
408445 processingStatus : EvidenceProcessingStatus . PENDING ,
409446 jobId : null ,
447+ quotaReservationId : reservation . reservationId ,
410448 } ) ;
411449 const savedEvidence = await this . evidenceRepository . save ( evidence ) ;
412450
413451 // Enqueue background processing job
414- const job = await this . jobQueueService . addEvidenceProcessingJob ( {
415- evidenceId : savedEvidence . id ,
416- disputeId,
417- storagePath,
418- mimeType : file . mimetype ,
419- originalFilename : file . originalname ,
420- uploadedBy : dto . uploadedBy ,
421- } ) ;
452+ let job ;
453+ try {
454+ job = await this . jobQueueService . addEvidenceProcessingJob ( {
455+ evidenceId : savedEvidence . id ,
456+ disputeId,
457+ storagePath,
458+ mimeType : file . mimetype ,
459+ originalFilename : file . originalname ,
460+ uploadedBy : dto . uploadedBy ,
461+ } ) ;
462+ } catch ( err ) {
463+ await this . quotaService . release ( reservation . reservationId , {
464+ reason : 'job-enqueue-failed' ,
465+ } ) ;
466+ throw err ;
467+ }
422468
423469 // Store job ID on the evidence record for status polling
424470 await this . evidenceRepository . update ( savedEvidence . id , {
0 commit comments