Skip to content

Commit 0c49d95

Browse files
feat(6515): update MATS submission service to handle files separately from metadata
1 parent c095efa commit 0c49d95

9 files changed

+428
-194
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@
8383
"ts-loader": "^6.2.1",
8484
"ts-node": "^8.6.2",
8585
"tsconfig-paths": "^4.2.0",
86-
"typescript": "^5.8.3"
86+
"typescript": "^5.8.3",
87+
"typescript-eslint": "^8.31.1"
8788
},
8889
"jest": {
8990
"moduleFileExtensions": [

src/dto/mats-data-submission-create-payload.dto.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator';
33

44
import { MatsDataSubmissionBaseDTO } from '../dto/mats-data-submission.dto';
55

6-
export class MatsDataSubmissionFilePathsDTO {
6+
export class MatsDataSubmissionFileNamesDTO {
77
@IsString()
88
@IsOptional()
99
ertFile?: string;
@@ -20,8 +20,8 @@ export class MatsDataSubmissionFilePathsDTO {
2020

2121
export class MatsDataSubmissionCreatePayloadDTO {
2222
@ValidateNested()
23-
@Type(() => MatsDataSubmissionFilePathsDTO)
24-
filePaths: MatsDataSubmissionFilePathsDTO;
23+
@Type(() => MatsDataSubmissionFileNamesDTO)
24+
fileNames: MatsDataSubmissionFileNamesDTO;
2525

2626
@ValidateNested()
2727
@Type(() => MatsDataSubmissionBaseDTO)

src/mats-data-submission/mats-data-submission-checks.service.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EntityManager } from 'typeorm';
55

66
import { MatsDataSubmissionBaseDTO } from '../dto/mats-data-submission.dto';
77
import { MatsReportTypeCode } from '../entities/mats-report-type-code.entity';
8-
import { MatsDataSubmissionFiles } from '../interfaces/mats-data-submission-files';
8+
import { MatsDataSubmissionFileNamesDTO } from '../dto/mats-data-submission-create-payload.dto';
99
import { MatsDataSubmissionMap } from '../maps/mats-data-submission.map';
1010
import { MatsDataSubmissionRepository } from './mats-data-submission.repository';
1111
import { MatsDataSubmissionService } from './mats-data-submission.service';
@@ -86,7 +86,8 @@ describe('MatsDataSubmissionChecksService', () => {
8686

8787
const result = await service.runChecks(
8888
mockPayload,
89-
{} as MatsDataSubmissionFiles,
89+
{} as MatsDataSubmissionFileNamesDTO,
90+
'locationId',
9091
);
9192
expect(result).toEqual([]);
9293
});

src/mats-data-submission/mats-data-submission-checks.service.ts

Lines changed: 93 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { Injectable } from '@nestjs/common';
22
import { Logger } from '@us-epa-camd/easey-common/logger';
3-
import { validate } from 'class-validator';
43
import { EntityManager, In } from 'typeorm';
54

6-
import { throwIfErrors } from '../utilities/functions';
5+
import { MatsDataSubmissionFileNamesDTO } from '../dto/mats-data-submission-create-payload.dto';
76
import { MatsDataSubmissionBaseDTO } from '../dto/mats-data-submission.dto';
87
import { MatsFileTypeCode } from '../entities/mats-file-type-code.entity';
98
import { MatsPollutantCode } from '../entities/mats-pollutant-code.entity';
109
import { MatsReportTypeCode } from '../entities/mats-report-type-code.entity';
1110
import { MatsTestMethodCode } from '../entities/mats-test-method-code.entity';
12-
import { MatsDataSubmissionService } from './mats-data-submission.service';
11+
import { throwIfErrors } from '../utilities/functions';
12+
import {
13+
MatsDataSubmissionService,
14+
METADATA_XML_FILE_NAME,
15+
} from './mats-data-submission.service';
1316

1417
@Injectable()
1518
export class MatsDataSubmissionChecksService {
@@ -21,6 +24,14 @@ export class MatsDataSubmissionChecksService {
2124
this.logger.setContext(MatsDataSubmissionChecksService.name);
2225
}
2326

27+
private getMimeType = async (fileName: string, locationId: string) => {
28+
const filePath = this.matsDataSubmissionService.createStagingFilePath(
29+
locationId,
30+
fileName,
31+
);
32+
return this.matsDataSubmissionService.getRemoteFileMimeType(filePath);
33+
};
34+
2435
private async pollutantToTestMethodCrosscheck(
2536
selectedPollutants: MatsPollutantCode[] = [],
2637
selectedTestMethods: MatsTestMethodCode[] = [],
@@ -88,23 +99,15 @@ export class MatsDataSubmissionChecksService {
8899

89100
async runChecks(
90101
metadata: MatsDataSubmissionBaseDTO,
91-
files: MatsDataSubmissionFiles,
102+
fileNames: MatsDataSubmissionFileNamesDTO,
103+
locationId: string,
92104
): Promise<Array<string>> {
93105
const errors: string[] = [];
94106

95-
// Validate the DTO.
96-
const dtoErrors = await validate(metadata, {
97-
groups: [metadata.reportTypeCode],
98-
});
99-
errors.push(...dtoErrors.map((e) => Object.values(e.constraints)).flat());
100-
101-
// Throw immediately if initial validation fails.
102-
throwIfErrors(errors, { asArray: true });
103-
104107
// Conditional validation of `testNumber`.
105108
if (
106109
metadata.reportTypeCode === 'NOTIFY' &&
107-
files.ertFile &&
110+
fileNames.ertFile &&
108111
!metadata.testNumber
109112
) {
110113
errors.push(
@@ -157,7 +160,11 @@ export class MatsDataSubmissionChecksService {
157160
throwIfErrors(errors, { asArray: true });
158161

159162
// Validate the provided files.
160-
const warnings = await this.validateFiles(reportType, files);
163+
const warnings = await this.validateFiles(
164+
reportType,
165+
fileNames,
166+
locationId,
167+
);
161168

162169
/* CROSSCHECK VALIDATION */
163170

@@ -217,22 +224,34 @@ export class MatsDataSubmissionChecksService {
217224

218225
private async validateFiles(
219226
reportType: MatsReportTypeCode,
220-
files: MatsDataSubmissionFiles,
227+
fileNames: MatsDataSubmissionFileNamesDTO,
228+
locationId: string,
221229
): Promise<string[]> {
222230
const errors: string[] = [];
223231
const warnings: string[] = [];
224232

225-
errors.push(...this.validateFileMimetypes(files));
233+
const fileNameErrors = this.validateFileNames(fileNames);
234+
if (fileNameErrors.length) {
235+
errors.push(...fileNameErrors);
236+
}
237+
238+
const mimetypeErrors = await this.validateFileMimetypes(
239+
fileNames,
240+
locationId,
241+
);
242+
if (mimetypeErrors.length) errors.push(...mimetypeErrors);
226243

227244
const attachmentErrors = await this.validateFileAttachments(
228-
files,
245+
fileNames,
229246
reportType,
247+
locationId,
230248
);
231-
232-
if (reportType.enforceAttachmentRules) {
233-
errors.push(...attachmentErrors);
234-
} else {
235-
warnings.push(...attachmentErrors);
249+
if (attachmentErrors.length) {
250+
if (reportType.enforceAttachmentRules) {
251+
errors.push(...attachmentErrors);
252+
} else {
253+
warnings.push(...attachmentErrors);
254+
}
236255
}
237256

238257
throwIfErrors(errors, { asArray: true });
@@ -241,12 +260,13 @@ export class MatsDataSubmissionChecksService {
241260
}
242261

243262
private async validateFileAttachments(
244-
files: MatsDataSubmissionFiles,
263+
fileNames: MatsDataSubmissionFileNamesDTO,
245264
reportType: MatsReportTypeCode,
265+
locationId: string,
246266
) {
247267
const errors: string[] = [];
248268

249-
const { ertFile, payloadFile, supportingFiles } = files;
269+
const { ertFile, payloadFile, supportingFiles } = fileNames;
250270

251271
const fileTypes = await this.entityManager.find(MatsFileTypeCode);
252272
const ertFileCheck = () => {
@@ -280,7 +300,8 @@ export class MatsDataSubmissionChecksService {
280300
// A payload PDF file OR (ERT file & at least one supporting file) are required.
281301
let hasRequiredFiles = true;
282302
if (payloadFile) {
283-
if (payloadFile.mimetype !== 'application/pdf') {
303+
const mimetype = await this.getMimeType(payloadFile, locationId);
304+
if (mimetype !== 'application/pdf') {
284305
hasRequiredFiles = false;
285306
}
286307
} else if (!ertFile || !supportingFiles?.length) {
@@ -309,42 +330,67 @@ export class MatsDataSubmissionChecksService {
309330
return errors;
310331
}
311332

312-
private validateFileMimetypes(files: MatsDataSubmissionFiles) {
333+
private async validateFileMimetypes(
334+
fileNames: MatsDataSubmissionFileNamesDTO,
335+
locationId: string,
336+
) {
313337
const errors: string[] = [];
314338

315-
const { ertFile, payloadFile, supportingFiles } = files;
339+
const { ertFile, payloadFile, supportingFiles } = fileNames;
316340

317341
// ERT file must be XML.
318-
if (ertFile && ertFile?.mimetype !== 'text/xml') {
319-
errors.push(
320-
`Expected ERT file to be of type XML, but got ${ertFile.mimetype}`,
321-
);
342+
if (ertFile) {
343+
const mimetype = await this.getMimeType(ertFile, locationId);
344+
if (!['application/xml', 'text/xml'].includes(mimetype)) {
345+
errors.push(`Expected ERT file to be of type XML, but got ${mimetype}`);
346+
}
322347
}
323348

324349
// Payload file must be PDF, JSON, or XML.
325350
const validPayloadFileTypes = [
326-
'application/pdf',
327351
'application/json',
352+
'application/pdf',
353+
'application/xml',
354+
'text/json',
328355
'text/xml',
329356
];
330-
if (payloadFile && !validPayloadFileTypes.includes(payloadFile.mimetype)) {
331-
errors.push(
332-
`Expected Payload file to be of type ${validPayloadFileTypes.join(
333-
', ',
334-
)} but got ${payloadFile.mimetype}`,
335-
);
357+
if (payloadFile) {
358+
const mimetype = await this.getMimeType(payloadFile, locationId);
359+
if (!validPayloadFileTypes.includes(mimetype)) {
360+
errors.push(
361+
`Expected Payload file to be of type ${validPayloadFileTypes.join(
362+
', ',
363+
)} but got ${mimetype}`,
364+
);
365+
}
336366
}
337367

338368
// Supporting files must be PDF.
339-
if (
340-
supportingFiles &&
341-
!supportingFiles.every((file) => file.mimetype === 'application/pdf')
342-
) {
343-
errors.push(
344-
`Expected Supporting files to be of type PDF, but got ${supportingFiles
345-
.map((file) => file.mimetype)
346-
.join(', ')}`,
369+
if (supportingFiles) {
370+
const mimetypes = await Promise.all(
371+
supportingFiles.map((file) => this.getMimeType(file, locationId)),
347372
);
373+
if (!mimetypes.every((mimetype) => mimetype === 'application/pdf')) {
374+
errors.push(
375+
`Expected Supporting files to be of type PDF, but got ${mimetypes.join(', ')}`,
376+
);
377+
}
378+
}
379+
380+
return errors;
381+
}
382+
383+
private validateFileNames(fileNames: MatsDataSubmissionFileNamesDTO) {
384+
const errors = [];
385+
386+
const fileNamesFlat = Object.values(fileNames).flat();
387+
388+
if (fileNamesFlat.length !== new Set(fileNamesFlat).size) {
389+
errors.push('File names must be unique.');
390+
}
391+
392+
if (fileNamesFlat.some((name) => name === METADATA_XML_FILE_NAME)) {
393+
errors.push(`File name [${METADATA_XML_FILE_NAME}] is reserved.`);
348394
}
349395

350396
return errors;

src/mats-data-submission/mats-data-submission.controller.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { LoggerModule } from '@us-epa-camd/easey-common/logger';
66
import { EntityManager } from 'typeorm';
77
import { DataSource } from 'typeorm';
88

9-
import { MatsDataSubmissionDTO } from '../dto/mats-data-submission.dto';
9+
import { MatsDataSubmissionCreatePayloadDTO } from '../dto/mats-data-submission-create-payload.dto';
1010
import { MatsDataSubmissionMap } from '../maps/mats-data-submission.map';
1111
import { MatsDataSubmissionChecksService } from './mats-data-submission-checks.service';
1212
import { MatsDataSubmissionController } from './mats-data-submission.controller';
@@ -22,6 +22,8 @@ const user: CurrentUser = {
2222
roles: [],
2323
};
2424

25+
const payload = new MatsDataSubmissionCreatePayloadDTO();
26+
2527
describe('MatsDataSubmissionController', () => {
2628
let controller: MatsDataSubmissionController;
2729
let checksService: MatsDataSubmissionChecksService;
@@ -70,7 +72,11 @@ describe('MatsDataSubmissionController', () => {
7072
it('should call the service to create a new submission', async () => {
7173
checksService.runChecks = jest.fn().mockResolvedValue([]);
7274
service.initializeMatsDataSubmission = jest.fn().mockResolvedValue('1');
73-
const res = await controller.initializeMatsDataSubmission('{}', {}, user);
75+
const res = await controller.initializeMatsDataSubmission(
76+
payload,
77+
user,
78+
'locationId',
79+
);
7480
expect(res).toEqual({ warnings: [], id: '1' });
7581
});
7682
});

0 commit comments

Comments
 (0)