@uniorganization/uni-lib 4.0.38 → 4.0.39

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.
@@ -16,6 +16,8 @@ export declare class TicketServiceCenterMatchProcessEntity {
16
16
  sourceFileId: number | null;
17
17
  outputFile?: FileEntity | null;
18
18
  outputFileId: number | null;
19
+ excelOutputFile?: FileEntity | null;
20
+ excelOutputFileId: number | null;
19
21
  createdDate: Date;
20
22
  updatedDate: Date;
21
23
  deletedDate: Date | null;
@@ -76,6 +76,15 @@ __decorate([
76
76
  (0, typeorm_1.Column)({ name: 'output_file_id', type: 'integer', nullable: true }),
77
77
  __metadata("design:type", Number)
78
78
  ], TicketServiceCenterMatchProcessEntity.prototype, "outputFileId", void 0);
79
+ __decorate([
80
+ (0, typeorm_1.ManyToOne)(() => file_entity_1.FileEntity),
81
+ (0, typeorm_1.JoinColumn)({ name: 'excel_output_file_id' }),
82
+ __metadata("design:type", file_entity_1.FileEntity)
83
+ ], TicketServiceCenterMatchProcessEntity.prototype, "excelOutputFile", void 0);
84
+ __decorate([
85
+ (0, typeorm_1.Column)({ name: 'excel_output_file_id', type: 'integer', nullable: true }),
86
+ __metadata("design:type", Number)
87
+ ], TicketServiceCenterMatchProcessEntity.prototype, "excelOutputFileId", void 0);
79
88
  __decorate([
80
89
  (0, typeorm_1.CreateDateColumn)({ name: 'created_date', type: 'timestamp' }),
81
90
  __metadata("design:type", Date)
@@ -1,3 +1,4 @@
1
1
  export * from './feedback-role.enum';
2
2
  export * from './service-center-status.enum';
3
3
  export * from './ticket-service-center-match-process-status.enum';
4
+ export * from './ticket-service-center-match-progress-phase.enum';
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./feedback-role.enum"), exports);
18
18
  __exportStar(require("./service-center-status.enum"), exports);
19
19
  __exportStar(require("./ticket-service-center-match-process-status.enum"), exports);
20
+ __exportStar(require("./ticket-service-center-match-progress-phase.enum"), exports);
@@ -0,0 +1,12 @@
1
+ export declare enum TicketServiceCenterMatchProgressPhase {
2
+ PENDING = "PENDING",
3
+ PARSING = "PARSING",
4
+ MATCHING = "MATCHING",
5
+ GENERATING_CSV = "GENERATING_CSV",
6
+ GENERATING_EXCEL = "GENERATING_EXCEL",
7
+ UPLOADING_CSV = "UPLOADING_CSV",
8
+ UPLOADING_EXCEL = "UPLOADING_EXCEL",
9
+ FINALIZING = "FINALIZING",
10
+ COMPLETED = "COMPLETED",
11
+ FAILED = "FAILED"
12
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TicketServiceCenterMatchProgressPhase = void 0;
4
+ var TicketServiceCenterMatchProgressPhase;
5
+ (function (TicketServiceCenterMatchProgressPhase) {
6
+ TicketServiceCenterMatchProgressPhase["PENDING"] = "PENDING";
7
+ TicketServiceCenterMatchProgressPhase["PARSING"] = "PARSING";
8
+ TicketServiceCenterMatchProgressPhase["MATCHING"] = "MATCHING";
9
+ TicketServiceCenterMatchProgressPhase["GENERATING_CSV"] = "GENERATING_CSV";
10
+ TicketServiceCenterMatchProgressPhase["GENERATING_EXCEL"] = "GENERATING_EXCEL";
11
+ TicketServiceCenterMatchProgressPhase["UPLOADING_CSV"] = "UPLOADING_CSV";
12
+ TicketServiceCenterMatchProgressPhase["UPLOADING_EXCEL"] = "UPLOADING_EXCEL";
13
+ TicketServiceCenterMatchProgressPhase["FINALIZING"] = "FINALIZING";
14
+ TicketServiceCenterMatchProgressPhase["COMPLETED"] = "COMPLETED";
15
+ TicketServiceCenterMatchProgressPhase["FAILED"] = "FAILED";
16
+ })(TicketServiceCenterMatchProgressPhase || (exports.TicketServiceCenterMatchProgressPhase = TicketServiceCenterMatchProgressPhase = {}));
@@ -8,6 +8,7 @@ export declare const FILE_STORAGE_BUCKET_ENV_KEYS: readonly ["BUCKET_NAME", "BUC
8
8
  export declare const FILE_TYPE_CODES: {
9
9
  readonly SERVICE_CENTER_TICKET_MATCH_SOURCE: "SERVICE_CENTER_TICKET_MATCH_SOURCE";
10
10
  readonly SERVICE_CENTER_TICKET_MATCH_OUTPUT: "SERVICE_CENTER_TICKET_MATCH_OUTPUT";
11
+ readonly SERVICE_CENTER_TICKET_MATCH_EXCEL_OUTPUT: "SERVICE_CENTER_TICKET_MATCH_EXCEL_OUTPUT";
11
12
  };
12
13
  export declare const FILE_STORAGE_DEFAULT_BUCKET_PREFIX = "";
13
14
  export declare const FILE_STORAGE_ALLOWED_PATH_SEPARATOR = "/";
@@ -18,6 +18,7 @@ exports.FILE_STORAGE_BUCKET_ENV_KEYS = [
18
18
  exports.FILE_TYPE_CODES = {
19
19
  SERVICE_CENTER_TICKET_MATCH_SOURCE: 'SERVICE_CENTER_TICKET_MATCH_SOURCE',
20
20
  SERVICE_CENTER_TICKET_MATCH_OUTPUT: 'SERVICE_CENTER_TICKET_MATCH_OUTPUT',
21
+ SERVICE_CENTER_TICKET_MATCH_EXCEL_OUTPUT: 'SERVICE_CENTER_TICKET_MATCH_EXCEL_OUTPUT',
21
22
  };
22
23
  exports.FILE_STORAGE_DEFAULT_BUCKET_PREFIX = '';
23
24
  exports.FILE_STORAGE_ALLOWED_PATH_SEPARATOR = '/';
@@ -20,6 +20,7 @@ export declare class FileStorageService {
20
20
  private readonly s3Client;
21
21
  constructor(fileRepository: Repository<FileEntity>, fileTypeRepository: Repository<FileTypeEntity>);
22
22
  uploadDocument(file: FileStorageUploadFile, options: FileStorageUploadOptions): Promise<FileEntity>;
23
+ deleteDocument(file: Pick<FileEntity, 'id' | 'path'>): Promise<void>;
23
24
  downloadDocument(bucketPath: string): Promise<Buffer>;
24
25
  resolveFileType(fileTypeCode?: string, fileTypeId?: number): Promise<FileTypeEntity>;
25
26
  private createS3Client;
@@ -74,11 +74,18 @@ let FileStorageService = FileStorageService_1 = class FileStorageService {
74
74
  return yield this.fileRepository.save(fileRecord);
75
75
  }
76
76
  catch (error) {
77
- yield this.deleteObject(bucketName, bucketPath);
77
+ yield this.deleteObject(bucketName, bucketPath, true);
78
78
  throw error;
79
79
  }
80
80
  });
81
81
  }
82
+ deleteDocument(file) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const bucketName = this.getRequiredEnv(constants_1.FILE_STORAGE_BUCKET_NAME_ENV);
85
+ yield this.deleteObject(bucketName, file.path);
86
+ yield this.fileRepository.softDelete(file.id);
87
+ });
88
+ }
82
89
  downloadDocument(bucketPath) {
83
90
  return __awaiter(this, void 0, void 0, function* () {
84
91
  const bucketName = this.getRequiredEnv(constants_1.FILE_STORAGE_BUCKET_NAME_ENV);
@@ -169,8 +176,8 @@ let FileStorageService = FileStorageService_1 = class FileStorageService {
169
176
  }
170
177
  });
171
178
  }
172
- deleteObject(bucketName, bucketPath) {
173
- return __awaiter(this, void 0, void 0, function* () {
179
+ deleteObject(bucketName_1, bucketPath_1) {
180
+ return __awaiter(this, arguments, void 0, function* (bucketName, bucketPath, suppressError = false) {
174
181
  try {
175
182
  yield this.s3Client.send(new client_s3_1.DeleteObjectCommand({
176
183
  Bucket: bucketName,
@@ -179,7 +186,10 @@ let FileStorageService = FileStorageService_1 = class FileStorageService {
179
186
  }
180
187
  catch (error) {
181
188
  const errorMessage = error instanceof Error ? error.message : String(error);
182
- this.logger.warn(`Uploaded object "${bucketPath}" could not be deleted after DB save failure: ${errorMessage}`);
189
+ this.logger.warn(`Object "${bucketPath}" could not be deleted: ${errorMessage}`);
190
+ if (!suppressError) {
191
+ throw error;
192
+ }
183
193
  }
184
194
  });
185
195
  }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const client_s3_1 = require("@aws-sdk/client-s3");
13
+ const file_storage_service_1 = require("./file-storage.service");
14
+ const BUCKET_ENV_KEYS = [
15
+ 'BUCKET_NAME',
16
+ 'BUCKET_REGION',
17
+ 'BUCKET_ACCESS_KEY_ID',
18
+ 'BUCKET_SECRET_ACCESS_KEY',
19
+ 'BUCKET_ENDPOINT',
20
+ ];
21
+ describe('FileStorageService', () => {
22
+ let originalBucketEnv;
23
+ let fileRepository;
24
+ let fileTypeRepository;
25
+ let service;
26
+ let send;
27
+ beforeEach(() => {
28
+ originalBucketEnv = Object.fromEntries(BUCKET_ENV_KEYS.map((key) => [key, process.env[key]]));
29
+ process.env.BUCKET_NAME = 'documents';
30
+ process.env.BUCKET_REGION = 'us-east-1';
31
+ process.env.BUCKET_ACCESS_KEY_ID = 'access-key';
32
+ process.env.BUCKET_SECRET_ACCESS_KEY = 'secret-key';
33
+ process.env.BUCKET_ENDPOINT = 'https://storage.example.com';
34
+ fileRepository = {
35
+ softDelete: jest.fn().mockResolvedValue({ raw: [], affected: 1 }),
36
+ create: jest.fn(),
37
+ save: jest.fn(),
38
+ };
39
+ fileTypeRepository = { findOne: jest.fn() };
40
+ service = new file_storage_service_1.FileStorageService(fileRepository, fileTypeRepository);
41
+ jest.spyOn(service.logger, 'warn').mockImplementation(() => undefined);
42
+ send = jest.fn().mockResolvedValue({});
43
+ service.s3Client.send = send;
44
+ });
45
+ afterEach(() => {
46
+ jest.restoreAllMocks();
47
+ for (const key of BUCKET_ENV_KEYS) {
48
+ const originalValue = originalBucketEnv[key];
49
+ if (originalValue === undefined) {
50
+ delete process.env[key];
51
+ }
52
+ else {
53
+ process.env[key] = originalValue;
54
+ }
55
+ }
56
+ });
57
+ describe('deleteDocument', () => {
58
+ it('deletes the bucket object before soft-deleting file metadata', () => __awaiter(void 0, void 0, void 0, function* () {
59
+ yield service.deleteDocument({ id: 44, path: 'outputs/file.csv' });
60
+ expect(send).toHaveBeenCalledTimes(1);
61
+ const command = send.mock.calls[0][0];
62
+ expect(command).toBeInstanceOf(client_s3_1.DeleteObjectCommand);
63
+ expect(command.input).toEqual({
64
+ Bucket: 'documents',
65
+ Key: 'outputs/file.csv',
66
+ });
67
+ expect(fileRepository.softDelete).toHaveBeenCalledWith(44);
68
+ expect(send.mock.invocationCallOrder[0]).toBeLessThan(fileRepository.softDelete.mock.invocationCallOrder[0]);
69
+ }));
70
+ it('rejects without soft-deleting metadata when bucket deletion fails', () => __awaiter(void 0, void 0, void 0, function* () {
71
+ const deletionError = new Error('bucket unavailable');
72
+ send.mockRejectedValue(deletionError);
73
+ yield expect(service.deleteDocument({ id: 44, path: 'outputs/file.csv' })).rejects.toBe(deletionError);
74
+ expect(fileRepository.softDelete).not.toHaveBeenCalled();
75
+ }));
76
+ });
77
+ describe('uploadDocument', () => {
78
+ it('preserves the DB save error when rollback object deletion fails', () => __awaiter(void 0, void 0, void 0, function* () {
79
+ const dbSaveError = new Error('distinctive DB save failure');
80
+ const cleanupError = new Error('rollback delete failure');
81
+ const fileRecord = { id: 44, path: 'pending/file.csv' };
82
+ fileTypeRepository.findOne.mockResolvedValue({
83
+ id: 7,
84
+ code: 'SOURCE_FILES',
85
+ });
86
+ fileRepository.create.mockReturnValue(fileRecord);
87
+ fileRepository.save.mockRejectedValue(dbSaveError);
88
+ send.mockImplementation((command) => {
89
+ if (command instanceof client_s3_1.PutObjectCommand) {
90
+ return Promise.resolve({});
91
+ }
92
+ if (command instanceof client_s3_1.DeleteObjectCommand) {
93
+ return Promise.reject(cleanupError);
94
+ }
95
+ throw new Error('Unexpected S3 command');
96
+ });
97
+ yield expect(service.uploadDocument({
98
+ originalname: 'tickets.csv',
99
+ mimetype: 'text/csv',
100
+ buffer: Buffer.from('ticket'),
101
+ }, { fileTypeCode: 'SOURCE_FILES' })).rejects.toBe(dbSaveError);
102
+ expect(send).toHaveBeenCalledTimes(2);
103
+ expect(send.mock.calls[0][0]).toBeInstanceOf(client_s3_1.PutObjectCommand);
104
+ expect(send.mock.calls[1][0]).toBeInstanceOf(client_s3_1.DeleteObjectCommand);
105
+ }));
106
+ });
107
+ });
@@ -1,3 +1,4 @@
1
1
  export * from './object.utils';
2
2
  export * from './string.utils';
3
3
  export * from './parse.util';
4
+ export * from './ticket-service-center-match-progress.util';
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./object.utils"), exports);
18
18
  __exportStar(require("./string.utils"), exports);
19
19
  __exportStar(require("./parse.util"), exports);
20
+ __exportStar(require("./ticket-service-center-match-progress.util"), exports);
@@ -0,0 +1,9 @@
1
+ import { TicketServiceCenterMatchProgressPhase } from '../enums/ticket-service-center-match-progress-phase.enum';
2
+ export interface TicketServiceCenterMatchProgressState {
3
+ phase: TicketServiceCenterMatchProgressPhase;
4
+ totalTickets: number | null;
5
+ previousPhase?: TicketServiceCenterMatchProgressPhase;
6
+ error?: string;
7
+ }
8
+ export declare const serializeTicketServiceCenterMatchProgress: (state: TicketServiceCenterMatchProgressState) => string;
9
+ export declare const parseTicketServiceCenterMatchProgress: (value: string | null | undefined) => TicketServiceCenterMatchProgressState | null;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTicketServiceCenterMatchProgress = exports.serializeTicketServiceCenterMatchProgress = void 0;
4
+ const ticket_service_center_match_progress_phase_enum_1 = require("../enums/ticket-service-center-match-progress-phase.enum");
5
+ const progressPhases = new Set(Object.values(ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase));
6
+ const isProgressPhase = (value) => typeof value === 'string' && progressPhases.has(value);
7
+ const isValidTotalTickets = (value) => value === null || (typeof value === 'number' && Number.isInteger(value) && value >= 0);
8
+ const serializeTicketServiceCenterMatchProgress = (state) => JSON.stringify(state);
9
+ exports.serializeTicketServiceCenterMatchProgress = serializeTicketServiceCenterMatchProgress;
10
+ const parseTicketServiceCenterMatchProgress = (value) => {
11
+ if (!value) {
12
+ return null;
13
+ }
14
+ try {
15
+ const state = JSON.parse(value);
16
+ if (typeof state !== 'object' || state === null || Array.isArray(state)) {
17
+ return null;
18
+ }
19
+ const candidate = state;
20
+ const { phase, totalTickets, previousPhase, error } = candidate;
21
+ if (!isProgressPhase(phase) || !isValidTotalTickets(totalTickets)) {
22
+ return null;
23
+ }
24
+ const progressState = {
25
+ phase,
26
+ totalTickets,
27
+ };
28
+ if (previousPhase !== undefined) {
29
+ if (!isProgressPhase(previousPhase)) {
30
+ return null;
31
+ }
32
+ progressState.previousPhase = previousPhase;
33
+ }
34
+ if (error !== undefined) {
35
+ if (typeof error !== 'string') {
36
+ return null;
37
+ }
38
+ progressState.error = error;
39
+ }
40
+ return progressState;
41
+ }
42
+ catch (_a) {
43
+ return null;
44
+ }
45
+ };
46
+ exports.parseTicketServiceCenterMatchProgress = parseTicketServiceCenterMatchProgress;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ticket_service_center_match_progress_phase_enum_1 = require("../enums/ticket-service-center-match-progress-phase.enum");
4
+ const ticket_service_center_match_progress_util_1 = require("./ticket-service-center-match-progress.util");
5
+ describe('ticket service center match progress', () => {
6
+ it('round-trips matching progress', () => {
7
+ const state = {
8
+ phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.MATCHING,
9
+ totalTickets: 100,
10
+ };
11
+ expect((0, ticket_service_center_match_progress_util_1.parseTicketServiceCenterMatchProgress)((0, ticket_service_center_match_progress_util_1.serializeTicketServiceCenterMatchProgress)(state))).toEqual(state);
12
+ });
13
+ it.each([
14
+ ['null', null],
15
+ ['empty text', ''],
16
+ ['legacy error text', 'Failed to process uploaded ticket file'],
17
+ ['malformed JSON', '{"phase":'],
18
+ ])('returns null for %s', (_description, value) => {
19
+ expect((0, ticket_service_center_match_progress_util_1.parseTicketServiceCenterMatchProgress)(value)).toBeNull();
20
+ });
21
+ it.each([
22
+ ['missing phase', { totalTickets: 10 }],
23
+ ['invalid phase', { phase: 'IMPORTING', totalTickets: 10 }],
24
+ ['missing totalTickets', { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.PARSING }],
25
+ ['negative totalTickets', { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.PARSING, totalTickets: -1 }],
26
+ ['fractional totalTickets', { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.PARSING, totalTickets: 1.5 }],
27
+ ['string totalTickets', { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.PARSING, totalTickets: '10' }],
28
+ [
29
+ 'invalid previousPhase',
30
+ { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.FAILED, totalTickets: 10, previousPhase: 'IMPORTING' },
31
+ ],
32
+ ['non-string error', { phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.FAILED, totalTickets: 10, error: 500 }],
33
+ ])('returns null for a state with %s', (_description, state) => {
34
+ expect((0, ticket_service_center_match_progress_util_1.parseTicketServiceCenterMatchProgress)(JSON.stringify(state))).toBeNull();
35
+ });
36
+ it('retains failure context', () => {
37
+ const state = {
38
+ phase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.FAILED,
39
+ previousPhase: ticket_service_center_match_progress_phase_enum_1.TicketServiceCenterMatchProgressPhase.UPLOADING_EXCEL,
40
+ totalTickets: 12,
41
+ error: 'Excel upload failed',
42
+ };
43
+ expect((0, ticket_service_center_match_progress_util_1.parseTicketServiceCenterMatchProgress)((0, ticket_service_center_match_progress_util_1.serializeTicketServiceCenterMatchProgress)(state))).toEqual(state);
44
+ });
45
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniorganization/uni-lib",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "description": "UNI Library",
5
5
  "author": "Jhomiguel",
6
6
  "main": "dist/index.js",