1- // @ts -nocheck
2-
3- import { ForbiddenException , Injectable , NotFoundException } from '@nestjs/common' ;
1+ import {
2+ ForbiddenException ,
3+ Injectable ,
4+ NotFoundException ,
5+ } from '@nestjs/common' ;
6+ import { Document , DocumentVersion , Prisma } from '@prisma/client' ;
47import { PrismaService } from '../database/prisma.service' ;
58import { AttachDocumentDto , AddVersionDto } from './dto/transaction-document.dto' ;
69
10+ /** Minimal transaction fields needed for access control. */
11+ type TxAccess = Pick < Prisma . TransactionGetPayload < object > , 'id' | 'buyerId' | 'sellerId' > ;
12+
13+ /** Document with its ordered versions. */
14+ type DocumentWithVersions = Document & { versions : DocumentVersion [ ] } ;
15+
16+ /** Document version with the uploader's public profile. */
17+ type VersionWithUploader = DocumentVersion & {
18+ uploadedBy : { id : string ; firstName : string ; lastName : string ; email : string } | null ;
19+ } ;
20+
721@Injectable ( )
822export class TransactionDocumentsService {
923 constructor ( private readonly prisma : PrismaService ) { }
1024
11- /** Attach a new document to a transaction and record version 1. */
12- async attach ( transactionId : string , dto : AttachDocumentDto , userId : string , userRole ?: string ) {
13- const tx = await this . ensureTransactionExists ( transactionId ) ;
25+ // ---------------------------------------------------------------------------
26+ // Helpers
27+ // ---------------------------------------------------------------------------
28+
29+ /**
30+ * Throws NotFoundException when the transaction does not exist.
31+ * Used only by `attach` which has no document to co-fetch.
32+ */
33+ private async fetchTxAccess ( transactionId : string ) : Promise < TxAccess > {
34+ const tx = await this . prisma . transaction . findUnique ( {
35+ where : { id : transactionId } ,
36+ select : { id : true , buyerId : true , sellerId : true } ,
37+ } ) ;
38+ if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
39+ return tx ;
40+ }
41+
42+ private assertAccess ( tx : TxAccess , userId : string , userRole ?: string ) : void {
43+ const isParty = tx . buyerId === userId || tx . sellerId === userId ;
44+ const isPrivileged = userRole === 'ADMIN' || userRole === 'AGENT' ;
45+ if ( ! isParty && ! isPrivileged ) {
46+ throw new ForbiddenException ( 'Access denied to this transaction document' ) ;
47+ }
48+ }
49+
50+ // ---------------------------------------------------------------------------
51+ // Public methods
52+ // ---------------------------------------------------------------------------
53+
54+ /**
55+ * Attach a new document to a transaction and record version 1.
56+ * 2 queries: (1) transaction access-select, (2) document create + version create
57+ * (version create is a separate write — not avoidable without denormalising).
58+ */
59+ async attach (
60+ transactionId : string ,
61+ dto : AttachDocumentDto ,
62+ userId : string ,
63+ userRole ?: string ,
64+ ) : Promise < Document > {
65+ const tx = await this . fetchTxAccess ( transactionId ) ;
1466 this . assertAccess ( tx , userId , userRole ) ;
1567
1668 const document = await this . prisma . document . create ( {
1769 data : {
1870 transactionId,
1971 userId,
20- documentType : dto . documentType as any ,
72+ documentType : dto . documentType ,
2173 fileName : dto . fileName ,
2274 fileUrl : dto . fileUrl ,
2375 fileSize : dto . fileSize ,
@@ -26,10 +78,9 @@ export class TransactionDocumentsService {
2678 stage : dto . stage ?? null ,
2779 category : dto . documentType . toLowerCase ( ) . replace ( '_' , '-' ) ,
2880 auditTrail : [ ] ,
29- } as any ,
81+ } ,
3082 } ) ;
3183
32- // Record initial version
3384 await this . prisma . documentVersion . create ( {
3485 data : {
3586 documentId : document . id ,
@@ -45,45 +96,88 @@ export class TransactionDocumentsService {
4596 return document ;
4697 }
4798
48- /** List all documents attached to a transaction. */
49- async list ( transactionId : string , userId : string , userRole : string ) {
50- const tx = await this . ensureTransactionExists ( transactionId ) ;
99+ /**
100+ * List all documents attached to a transaction.
101+ * Reduced from 2 sequential queries to 1 batched round-trip via
102+ * Prisma $transaction: transaction access-select + document list run
103+ * concurrently in the same connection checkout.
104+ */
105+ async list (
106+ transactionId : string ,
107+ userId : string ,
108+ userRole : string ,
109+ ) : Promise < DocumentWithVersions [ ] > {
110+ const [ tx , documents ] = await this . prisma . $transaction ( [
111+ this . prisma . transaction . findUnique ( {
112+ where : { id : transactionId } ,
113+ select : { id : true , buyerId : true , sellerId : true } ,
114+ } ) ,
115+ this . prisma . document . findMany ( {
116+ where : { transactionId } ,
117+ orderBy : { createdAt : 'desc' } ,
118+ include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
119+ } ) ,
120+ ] ) ;
121+
122+ if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
51123 this . assertAccess ( tx , userId , userRole ) ;
52- return this . prisma . document . findMany ( {
53- where : { transactionId } ,
54- orderBy : { createdAt : 'desc' } ,
55- include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
56- } ) ;
124+ return documents ;
57125 }
58126
59- /** Get a single document (must belong to the transaction). */
60- async findOne ( transactionId : string , documentId : string , userId : string , userRole : string ) {
61- const tx = await this . ensureTransactionExists ( transactionId ) ;
62- this . assertAccess ( tx , userId , userRole ) ;
127+ /**
128+ * Get a single document (must belong to the transaction).
129+ * Reduced from 2 sequential queries to 1 batched round-trip:
130+ * transaction access-select + document-with-versions fetched together.
131+ */
132+ async findOne (
133+ transactionId : string ,
134+ documentId : string ,
135+ userId : string ,
136+ userRole : string ,
137+ ) : Promise < DocumentWithVersions > {
138+ const [ tx , doc ] = await this . prisma . $transaction ( [
139+ this . prisma . transaction . findUnique ( {
140+ where : { id : transactionId } ,
141+ select : { id : true , buyerId : true , sellerId : true } ,
142+ } ) ,
143+ this . prisma . document . findFirst ( {
144+ where : { id : documentId , transactionId } ,
145+ include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
146+ } ) ,
147+ ] ) ;
63148
64- const doc = await this . prisma . document . findFirst ( {
65- where : { id : documentId , transactionId } ,
66- include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
67- } ) ;
149+ if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
150+ this . assertAccess ( tx , userId , userRole ) ;
68151 if ( ! doc ) throw new NotFoundException ( 'Document not found for this transaction' ) ;
69152 return doc ;
70153 }
71154
72- /** Add a new version to an existing transaction document. */
155+ /**
156+ * Add a new version to an existing transaction document.
157+ * Reduced from 2 sequential read queries to 1 batched round-trip:
158+ * transaction access-select + document-with-versions fetched together,
159+ * followed by the single write batch (version create + document update).
160+ */
73161 async addVersion (
74162 transactionId : string ,
75163 documentId : string ,
76164 dto : AddVersionDto ,
77165 userId : string ,
78166 userRole : string ,
79- ) {
80- const tx = await this . ensureTransactionExists ( transactionId ) ;
81- this . assertAccess ( tx , userId , userRole ) ;
167+ ) : Promise < DocumentVersion > {
168+ const [ tx , doc ] = await this . prisma . $transaction ( [
169+ this . prisma . transaction . findUnique ( {
170+ where : { id : transactionId } ,
171+ select : { id : true , buyerId : true , sellerId : true } ,
172+ } ) ,
173+ this . prisma . document . findFirst ( {
174+ where : { id : documentId , transactionId } ,
175+ include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
176+ } ) ,
177+ ] ) ;
82178
83- const doc = await this . prisma . document . findFirst ( {
84- where : { id : documentId , transactionId } ,
85- include : { versions : { orderBy : { versionNumber : 'asc' } } } ,
86- } ) ;
179+ if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
180+ this . assertAccess ( tx , userId , userRole ) ;
87181 if ( ! doc ) throw new NotFoundException ( 'Document not found for this transaction' ) ;
88182
89183 const nextVersion = ( doc . versions ?. length ?? 0 ) + 1 ;
@@ -100,7 +194,6 @@ export class TransactionDocumentsService {
100194 changeNote : dto . changeNote ,
101195 } ,
102196 } ) ,
103- // Update the document to point to the latest file
104197 this . prisma . document . update ( {
105198 where : { id : documentId } ,
106199 data : { fileUrl : dto . fileUrl , fileName : dto . fileName , fileSize : dto . fileSize } ,
@@ -110,47 +203,75 @@ export class TransactionDocumentsService {
110203 return version ;
111204 }
112205
113- /** List all versions of a document. */
114- async getVersions ( transactionId : string , documentId : string , userId : string , userRole : string ) {
115- const tx = await this . ensureTransactionExists ( transactionId ) ;
116- this . assertAccess ( tx , userId , userRole ) ;
117-
118- const doc = await this . prisma . document . findFirst ( {
119- where : { id : documentId , transactionId } ,
120- } ) ;
121- if ( ! doc ) throw new NotFoundException ( 'Document not found for this transaction' ) ;
122- return this . prisma . documentVersion . findMany ( {
123- where : { documentId } ,
124- orderBy : { versionNumber : 'asc' } ,
125- include : {
126- uploadedBy : { select : { id : true , firstName : true , lastName : true , email : true } } ,
127- } ,
128- } ) ;
129- }
206+ /**
207+ * List all versions of a document.
208+ *
209+ * Previously made 3 sequential round-trips:
210+ * 1. ensureTransactionExists
211+ * 2. document.findFirst (existence check only)
212+ * 3. documentVersion.findMany (with uploadedBy)
213+ *
214+ * Now consolidated to 1 batched round-trip: transaction access fields +
215+ * document-with-versions-and-uploader fetched together.
216+ */
217+ async getVersions (
218+ transactionId : string ,
219+ documentId : string ,
220+ userId : string ,
221+ userRole : string ,
222+ ) : Promise < VersionWithUploader [ ] > {
223+ const [ tx , doc ] = await this . prisma . $transaction ( [
224+ this . prisma . transaction . findUnique ( {
225+ where : { id : transactionId } ,
226+ select : { id : true , buyerId : true , sellerId : true } ,
227+ } ) ,
228+ this . prisma . document . findFirst ( {
229+ where : { id : documentId , transactionId } ,
230+ include : {
231+ versions : {
232+ orderBy : { versionNumber : 'asc' } ,
233+ include : {
234+ uploadedBy : {
235+ select : { id : true , firstName : true , lastName : true , email : true } ,
236+ } ,
237+ } ,
238+ } ,
239+ } ,
240+ } ) ,
241+ ] ) ;
130242
131- /** Remove a document from a transaction. */
132- async remove ( transactionId : string , documentId : string , userId : string , userRole : string ) {
133- const tx = await this . ensureTransactionExists ( transactionId ) ;
243+ if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
134244 this . assertAccess ( tx , userId , userRole ) ;
135-
136- const doc = await this . prisma . document . findFirst ( {
137- where : { id : documentId , transactionId } ,
138- } ) ;
139245 if ( ! doc ) throw new NotFoundException ( 'Document not found for this transaction' ) ;
140- return this . prisma . document . delete ( { where : { id : documentId } } ) ;
246+
247+ return doc . versions as VersionWithUploader [ ] ;
141248 }
142249
143- private async ensureTransactionExists ( transactionId : string ) {
144- const tx = await this . prisma . transaction . findUnique ( { where : { id : transactionId } } ) ;
250+ /**
251+ * Remove a document from a transaction.
252+ * Reduced from 2 sequential read queries to 1 batched round-trip:
253+ * transaction access-select + document existence check fetched together.
254+ */
255+ async remove (
256+ transactionId : string ,
257+ documentId : string ,
258+ userId : string ,
259+ userRole : string ,
260+ ) : Promise < Document > {
261+ const [ tx , doc ] = await this . prisma . $transaction ( [
262+ this . prisma . transaction . findUnique ( {
263+ where : { id : transactionId } ,
264+ select : { id : true , buyerId : true , sellerId : true } ,
265+ } ) ,
266+ this . prisma . document . findFirst ( {
267+ where : { id : documentId , transactionId } ,
268+ } ) ,
269+ ] ) ;
270+
145271 if ( ! tx ) throw new NotFoundException ( `Transaction ${ transactionId } not found` ) ;
146- return tx ;
147- }
272+ this . assertAccess ( tx , userId , userRole ) ;
273+ if ( ! doc ) throw new NotFoundException ( 'Document not found for this transaction' ) ;
148274
149- private assertAccess ( tx : any , userId : string , userRole ?: string ) {
150- const isParty = tx . buyerId === userId || tx . sellerId === userId ;
151- const isPrivileged = userRole === 'ADMIN' || userRole === 'AGENT' ;
152- if ( ! isParty && ! isPrivileged ) {
153- throw new ForbiddenException ( 'Access denied to this transaction document' ) ;
154- }
275+ return this . prisma . document . delete ( { where : { id : documentId } } ) ;
155276 }
156277}
0 commit comments