refactor(nestjs): attachments module

This commit is contained in:
Ahmed Bouhuolia
2025-04-06 21:13:46 +02:00
parent 1ed77dd5ed
commit 842a862b87
41 changed files with 918 additions and 859 deletions

View File

@@ -28,6 +28,7 @@
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@liaoliaots/nestjs-redis": "^10.0.0", "@liaoliaots/nestjs-redis": "^10.0.0",
"@types/multer": "^1.4.11",
"@nestjs/bull": "^10.2.1", "@nestjs/bull": "^10.2.1",
"@nestjs/bullmq": "^10.2.2", "@nestjs/bullmq": "^10.2.2",
"@nestjs/cache-manager": "^2.2.2", "@nestjs/cache-manager": "^2.2.2",
@@ -69,11 +70,14 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"mathjs": "^9.4.0", "mathjs": "^9.4.0",
"mime-types": "^2.1.35",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-range": "^4.0.2", "moment-range": "^4.0.2",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"mysql2": "^3.11.3", "mysql2": "^3.11.3",
"multer": "1.4.5-lts.1",
"multer-s3": "^3.0.1",
"nestjs-cls": "^5.2.0", "nestjs-cls": "^5.2.0",
"nestjs-i18n": "^10.4.9", "nestjs-i18n": "^10.4.9",
"nestjs-redis": "^1.3.3", "nestjs-redis": "^1.3.3",

View File

@@ -0,0 +1 @@
export const MULTER_MODULE_OPTIONS = 'MULTER_MODULE_OPTIONS';

View File

@@ -0,0 +1,24 @@
import type { Multer } from 'multer';
import * as multerS3 from 'multer-s3';
export const multerExceptions = {
// from https://github.com/expressjs/multer/blob/master/lib/multer-error.js
LIMIT_PART_COUNT: 'Too many parts',
LIMIT_FILE_SIZE: 'File too large',
LIMIT_FILE_COUNT: 'Too many files',
LIMIT_FIELD_KEY: 'Field name too long',
LIMIT_FIELD_VALUE: 'Field value too long',
LIMIT_FIELD_COUNT: 'Too many fields',
LIMIT_UNEXPECTED_FILE: 'Unexpected field',
MISSING_FIELD_NAME: 'Field name missing',
};
export const busboyExceptions = {
// from https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js
MULTIPART_BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
MULTIPART_MALFORMED_PART_HEADER: 'Malformed part header',
MULTIPART_UNEXPECTED_END_OF_FORM: 'Unexpected end of form',
MULTIPART_UNEXPECTED_END_OF_FILE: 'Unexpected end of file',
};

View File

@@ -0,0 +1,38 @@
import {
BadRequestException,
HttpException,
PayloadTooLargeException,
} from '@nestjs/common';
import { multerExceptions, busboyExceptions } from './multer.constants';
// Multer may add in a 'field' property to the error
// https://github.com/expressjs/multer/blob/aa42bea6ac7d0cb8fcb279b15a7278cda805dc63/lib/multer-error.js#L19
export function transformException(
error: (Error & { field?: string }) | undefined,
) {
if (!error || error instanceof HttpException) {
return error;
}
switch (error.message) {
case multerExceptions.LIMIT_FILE_SIZE:
return new PayloadTooLargeException(error.message);
case multerExceptions.LIMIT_FILE_COUNT:
case multerExceptions.LIMIT_FIELD_KEY:
case multerExceptions.LIMIT_FIELD_VALUE:
case multerExceptions.LIMIT_FIELD_COUNT:
case multerExceptions.LIMIT_UNEXPECTED_FILE:
case multerExceptions.LIMIT_PART_COUNT:
case multerExceptions.MISSING_FIELD_NAME:
if (error.field) {
return new BadRequestException(`${error.message} - ${error.field}`);
}
return new BadRequestException(error.message);
case busboyExceptions.MULTIPART_BOUNDARY_NOT_FOUND:
return new BadRequestException(error.message);
case busboyExceptions.MULTIPART_MALFORMED_PART_HEADER:
case busboyExceptions.MULTIPART_UNEXPECTED_END_OF_FORM:
case busboyExceptions.MULTIPART_UNEXPECTED_END_OF_FILE:
return new BadRequestException(`Multipart: ${error.message}`);
}
return error;
}

View File

@@ -0,0 +1,70 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import * as multer from 'multer';
import { Observable } from 'rxjs';
import { MULTER_MODULE_OPTIONS } from '../constants/files.constants';
import { transformException } from '../constants/multer.utils';
import { MulterModuleOptions } from '@nestjs/platform-express';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
type MulterInstance = any;
/**
* @param {string} fieldName
* @param {Function|MulterOptions} localOptions - Function that receives controller instance or MulterOptions object
*/
export function FileInterceptor(
fieldName: string,
localOptions?: ((instance: any) => MulterOptions) | MulterOptions,
): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
protected multer: MulterInstance;
constructor(
@Optional()
@Inject(MULTER_MODULE_OPTIONS)
options: (() => MulterModuleOptions | MulterModuleOptions) = () => ({}),
) {
const resolvedOptions = typeof localOptions === 'function'
? localOptions(this)
: localOptions;
this.multer = (multer as any)({
...(typeof options === 'function' ? options() : options),
...resolvedOptions,
});
}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
await new Promise<void>((resolve, reject) =>
this.multer.single(fieldName)(
ctx.getRequest(),
ctx.getResponse(),
(err: any) => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
},
),
);
return next.handle();
}
}
const Interceptor = mixin(MixinInterceptor);
return Interceptor;
}

View File

@@ -1,5 +1,6 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { S3Module } from "../S3/S3.module"; import * as multerS3 from 'multer-s3';
import { S3_CLIENT, S3Module } from "../S3/S3.module";
import { DeleteAttachment } from "./DeleteAttachment"; import { DeleteAttachment } from "./DeleteAttachment";
import { GetAttachment } from "./GetAttachment"; import { GetAttachment } from "./GetAttachment";
import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl"; import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
@@ -13,11 +14,26 @@ import { AttachmentsOnExpenses } from "./events/AttachmentsOnExpenses";
import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsReceived"; import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsReceived";
import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals"; import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals";
import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits"; import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits";
import { AttachmentsOnSaleInvoiceCreated } from "./events/AttachmentsOnSaleInvoice";
import { AttachmentsController } from "./Attachments.controller";
import { RegisterTenancyModel } from "../Tenancy/TenancyModels/Tenancy.module";
import { DocumentModel } from "./models/Document.model";
import { DocumentLinkModel } from "./models/DocumentLink.model";
import { AttachmentsApplication } from "./AttachmentsApplication";
import { UploadDocument } from "./UploadDocument";
import { AttachmentUploadPipeline } from "./S3UploadPipeline";
import { MULTER_MODULE_OPTIONS } from "@/common/constants/files.constants";
import { ConfigService } from "@nestjs/config";
import { S3Client } from "@aws-sdk/client-s3";
const models = [
RegisterTenancyModel(DocumentModel),
RegisterTenancyModel(DocumentLinkModel),
];
@Module({ @Module({
imports: [S3Module], imports: [S3Module, ...models],
controllers: [AttachmentsController],
providers: [ providers: [
DeleteAttachment, DeleteAttachment,
GetAttachment, GetAttachment,
@@ -31,7 +47,34 @@ import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits"
AttachmentsOnExpenses, AttachmentsOnExpenses,
AttachmentsOnPaymentsReceived, AttachmentsOnPaymentsReceived,
AttachmentsOnManualJournals, AttachmentsOnManualJournals,
AttachmentsOnVendorCredits AttachmentsOnVendorCredits,
AttachmentsOnSaleInvoiceCreated,
AttachmentsApplication,
UploadDocument,
AttachmentUploadPipeline,
{
provide: MULTER_MODULE_OPTIONS,
inject: [ConfigService, S3_CLIENT],
useFactory: (configService: ConfigService, s3: S3Client) => ({
storage: multerS3({
s3,
bucket: configService.get('s3.bucket'),
contentType: multerS3.AUTO_CONTENT_TYPE,
metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname });
},
key: function (req, file, cb) {
cb(null, Date.now().toString());
},
acl: function(req, file, cb) {
// Conditionally set file to public or private based on isPublic flag
const aclValue = true ? 'public-read' : 'private';
// Set ACL based on the isPublic flag
cb(null, aclValue);
}
}),
})
}
] ]
}) })
export class AttachmentsModule {} export class AttachmentsModule {}

View File

@@ -0,0 +1,202 @@
import mime from 'mime-types';
import { Response, NextFunction, Request } from 'express';
import {
ApiBody,
ApiConsumes,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Res,
UnauthorizedException,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import {
LinkAttachmentDto,
UnlinkAttachmentDto,
UploadAttachmentDto,
} from './dtos/Attachment.dto';
import { AttachmentsApplication } from './AttachmentsApplication';
import { AttachmentUploadPipeline } from './S3UploadPipeline';
import { FileInterceptor } from '@/common/interceptors/file.interceptor';
import { ConfigService } from '@nestjs/config';
@ApiTags('Attachments')
@Controller('/attachments')
export class AttachmentsController {
/**
* @param {AttachmentsApplication} attachmentsApplication - Attachments application.
* @param uploadPipelineService
*/
constructor(
private readonly attachmentsApplication: AttachmentsApplication,
private readonly uploadPipelineService: AttachmentUploadPipeline,
private readonly configService: ConfigService,
) {}
/**
* Uploads the attachments to S3 and store the file metadata to DB.
*/
@Post()
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@ApiOperation({ summary: 'Upload attachment to S3' })
@ApiBody({ description: 'Upload attachment', type: UploadAttachmentDto })
@ApiResponse({
status: 200,
description: 'The document has been uploaded successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - File upload failed',
})
async uploadAttachment(
@UploadedFile() file: Express.Multer.File,
res: Response,
next: NextFunction,
): Promise<Response | void> {
if (!file) {
throw new UnauthorizedException({
errorType: 'FILE_UPLOAD_FAILED',
message: 'Now file uploaded.',
});
}
const data = await this.attachmentsApplication.upload(file);
return res.status(200).send({
status: 200,
message: 'The document has uploaded successfully.',
data,
});
}
/**
* Retrieves the given attachment key.
*/
@Get('/:id')
@ApiOperation({ summary: 'Get attachment by ID' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiResponse({ status: 200, description: 'Returns the attachment file' })
async getAttachment(
@Res() res: Response,
@Param('id') documentId: string,
): Promise<Response | void> {
const data = await this.attachmentsApplication.get(documentId);
const byte = await data.Body.transformToByteArray();
const extension = mime.extension(data.ContentType);
const buffer = Buffer.from(byte);
res.set('Content-Disposition', `filename="${documentId}.${extension}"`);
res.set('Content-Type', data.ContentType);
res.send(buffer);
}
/**
* Deletes the given document key.
*/
@Delete('/:id')
@ApiOperation({ summary: 'Delete attachment by ID' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiResponse({
status: 200,
description: 'The document has been deleted successfully',
})
async deleteAttachment(
@Res() res: Response,
@Param('id') documentId: string,
): Promise<Response | void> {
await this.attachmentsApplication.delete(documentId);
return res.status(200).send({
status: 200,
message: 'The document has been delete successfully.',
});
}
/**
* Links the given document key.
*/
@Post('/:id/link')
@ApiOperation({ summary: 'Link attachment to a model' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiBody({ type: LinkAttachmentDto })
@ApiResponse({
status: 200,
description: 'The document has been linked successfully',
})
async linkDocument(
@Body() linkDocumentDto: LinkAttachmentDto,
@Param('id') documentId: string,
@Res() res: Response,
): Promise<Response | void> {
await this.attachmentsApplication.link(
documentId,
linkDocumentDto.modelRef,
linkDocumentDto.modelId,
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
}
/**
* Links the given document key.
*/
@Post('/:id/unlink')
@ApiOperation({ summary: 'Unlink attachment from a model' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiBody({ type: UnlinkAttachmentDto })
@ApiResponse({
status: 200,
description: 'The document has been unlinked successfully',
})
async unlinkDocument(
@Body() unlinkDto: UnlinkAttachmentDto,
@Param('id') documentId: string,
@Res() res: Response,
): Promise<Response | void> {
await this.attachmentsApplication.link(
documentId,
unlinkDto.modelRef,
unlinkDto.modelId,
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
}
/**
* Retreives the presigned url of the given attachment key.
*/
@Get('/:id/presigned-url')
@ApiOperation({ summary: 'Get presigned URL for attachment' })
@ApiParam({ name: 'id', description: 'Attachment ID' })
@ApiResponse({
status: 200,
description: 'Returns the presigned URL for the attachment',
})
async getAttachmentPresignedUrl(
@Param('id') documentKey: string,
res: Response,
next: NextFunction,
): Promise<Response | void> {
const presignedUrl =
await this.attachmentsApplication.getPresignedUrl(documentKey);
return res.status(200).send({ presignedUrl });
}
}

View File

@@ -19,7 +19,6 @@ export class AttachmentsApplication {
/** /**
* Saves the metadata of uploaded document to S3 on database. * Saves the metadata of uploaded document to S3 on database.
* @param {number} tenantId
* @param {} file * @param {} file
* @returns {Promise<Document>} * @returns {Promise<Document>}
*/ */
@@ -38,7 +37,6 @@ export class AttachmentsApplication {
/** /**
* Retrieves the document data. * Retrieves the document data.
* @param {number} tenantId
* @param {string} documentKey * @param {string} documentKey
*/ */
public get(documentKey: string) { public get(documentKey: string) {
@@ -58,7 +56,6 @@ export class AttachmentsApplication {
/** /**
* Unlinks the given document from resource model. * Unlinks the given document from resource model.
* @param {number} tenantId
* @param {string} filekey * @param {string} filekey
* @param {string} modelRef * @param {string} modelRef
* @param {number} modelId * @param {number} modelId
@@ -70,7 +67,6 @@ export class AttachmentsApplication {
/** /**
* Retrieves the presigned url of the given attachment key. * Retrieves the presigned url of the given attachment key.
* @param {number} tenantId
* @param {string} key * @param {string} key
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { S3_CLIENT } from '../S3/S3.module'; import { S3_CLIENT } from '../S3/S3.module';
import { DocumentModel } from './models/Document.model'; import { DocumentModel } from './models/Document.model';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
@@ -17,14 +17,14 @@ export class DeleteAttachment {
@Inject(S3_CLIENT) @Inject(S3_CLIENT)
private readonly s3Client: S3Client, private readonly s3Client: S3Client,
@Inject(Document.name) @Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>, private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
@Inject(DocumentLinkModel.name) @Inject(DocumentLinkModel.name)
private readonly documentLinkModel: TenantModelProxy<typeof DocumentLinkModel> private readonly documentLinkModel: TenantModelProxy<
) { typeof DocumentLinkModel
>,
} ) {}
/** /**
* Deletes the give file attachment file key. * Deletes the give file attachment file key.
@@ -37,13 +37,15 @@ export class DeleteAttachment {
}; };
await this.s3Client.send(new DeleteObjectCommand(params)); await this.s3Client.send(new DeleteObjectCommand(params));
const foundDocument = await this.documentModel().query() const foundDocument = await this.documentModel()
.query()
.findOne('key', filekey) .findOne('key', filekey)
.throwIfNotFound(); .throwIfNotFound();
await this.uow.withTransaction(async (trx: Knex.Transaction) => { await this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Delete all document links // Delete all document links
await this.documentLinkModel().query(trx) await this.documentLinkModel()
.query(trx)
.where('documentId', foundDocument.id) .where('documentId', foundDocument.id)
.delete(); .delete();

View File

@@ -9,13 +9,11 @@ export class GetAttachment {
private readonly configService: ConfigService, private readonly configService: ConfigService,
@Inject(S3_CLIENT) @Inject(S3_CLIENT)
private readonly s3: S3Client private readonly s3: S3Client,
) { ) {}
}
/** /**
* Retrieves data of the given document key. * Retrieves data of the given document key.
* @param {number} tenantId
* @param {string} filekey * @param {string} filekey
*/ */
async getAttachment(filekey: string) { async getAttachment(filekey: string) {

View File

@@ -1,35 +1,42 @@
import { GetObjectCommand } from '@aws-sdk/client-s3'; import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentModel } from './models/Document.model'; import { DocumentModel } from './models/Document.model';
import { ConfigService } from '@nestjs/config';
import { S3_CLIENT } from '../S3/S3.module';
@Injectable() @Injectable()
export class getAttachmentPresignedUrl { export class getAttachmentPresignedUrl {
constructor( constructor(
private readonly documentModel: TenantModelProxy<typeof DocumentModel> private readonly configService: ConfigService,
@Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
@Inject(S3_CLIENT)
private readonly s3Client: S3Client,
) {} ) {}
/** /**
* Retrieves the presigned url of the given attachment key with the original filename. * Retrieves the presigned url of the given attachment key with the original filename.
* @param {number} tenantId * @param {string} key -
* @param {string} key
* @returns {string} * @returns {string}
*/ */
async getPresignedUrl(tenantId: number, key: string) { async getPresignedUrl(key: string) {
const foundDocument = await this.documentModel().query().findOne({ key }); const foundDocument = await this.documentModel().query().findOne({ key });
const config = this.configService.get('s3');
let ResponseContentDisposition = 'attachment'; let ResponseContentDisposition = 'attachment';
if (foundDocument && foundDocument.originName) { if (foundDocument && foundDocument.originName) {
ResponseContentDisposition += `; filename="${foundDocument.originName}"`; ResponseContentDisposition += `; filename="${foundDocument.originName}"`;
} }
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: config.s3.bucket, Bucket: config.bucket,
Key: key, Key: key,
ResponseContentDisposition, ResponseContentDisposition,
}); });
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); const signedUrl = await getSignedUrl(this.s3Client, command, { expiresIn: 300 });
return signedUrl; return signedUrl;
} }

View File

@@ -1,46 +1,59 @@
import { Inject, Service } from 'typedi'; import { ModuleRef } from '@nestjs/core';
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
validateLinkModelEntryExists, validateLinkModelEntryExists,
validateLinkModelExists, validateLinkModelExists,
} from './Attachments/_utils'; } from './_utils';
import HasTenancyService from '../Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentLink } from '../ChromiumlyTenancy/models/DocumentLink';
import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '../Items/ServiceError';
import { getAttachableModelsMap } from './decorators/InjectAttachable.decorator';
import { DocumentModel } from './models/Document.model';
import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice';
@Service() @Injectable()
export class LinkAttachment { export class LinkAttachment {
@Inject() constructor(
private tenancy: HasTenancyService; private moduleRef: ModuleRef,
@Inject(DocumentLink.name)
private readonly documentLinkModel: TenantModelProxy<typeof DocumentLink>,
@Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
) {}
/** /**
* Links the given file key to the given model type and id. * Links the given file key to the given model type and id.
* @param {number} tenantId * @param {string} filekey - File key.
* @param {string} filekey * @param {string} modelRef - Model reference.
* @param {string} modelRef * @param {number} modelId - Model id.
* @param {number} modelId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async link( async link(
tenantId: number,
filekey: string, filekey: string,
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
) { ) {
const { DocumentLink, Document, ...models } = this.tenancy.models(tenantId); const attachmentsAttachableModels = getAttachableModelsMap();
const LinkModel = models[modelRef]; const attachableModel = attachmentsAttachableModels.get(modelRef);
validateLinkModelExists(LinkModel);
const foundFile = await Document.query(trx) validateLinkModelExists(attachableModel);
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
const foundFile = await this.documentModel()
.query(trx)
.findOne('key', filekey) .findOne('key', filekey)
.throwIfNotFound(); .throwIfNotFound();
const foundLinkModel = await LinkModel.query(trx).findById(modelId); const foundLinkModel = await LinkModel().query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel); validateLinkModelEntryExists(foundLinkModel);
const foundLinks = await DocumentLink.query(trx) const foundLinks = await this.documentLinkModel().query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.where('documentId', foundFile.id); .where('documentId', foundFile.id);
@@ -48,7 +61,7 @@ export class LinkAttachment {
if (foundLinks.length > 0) { if (foundLinks.length > 0) {
throw new ServiceError(ERRORS.DOCUMENT_LINK_ALREADY_LINKED); throw new ServiceError(ERRORS.DOCUMENT_LINK_ALREADY_LINKED);
} }
await DocumentLink.query(trx).insert({ await this.documentLinkModel().query(trx).insert({
modelRef, modelRef,
modelId, modelId,
documentId: foundFile.id, documentId: foundFile.id,
@@ -57,23 +70,21 @@ export class LinkAttachment {
/** /**
* Links the given file keys to the given model type and id. * Links the given file keys to the given model type and id.
* @param {number} tenantId * @param {string[]} filekeys - File keys.
* @param {string[]} filekeys * @param {string} modelRef - Model reference.
* @param {string} modelRef * @param {number} modelId - Model id.
* @param {number} modelId * @param {Knex.Transaction} trx - Knex transaction.
* @param {Knex.Transaction} trx
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async bulkLink( async bulkLink(
tenantId: number,
filekeys: string[], filekeys: string[],
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
) { ) {
return bluebird.each(filekeys, async (fieldKey: string) => { return bluebird.each(filekeys, async (fieldKey: string) => {
try { try {
await this.link(tenantId, fieldKey, modelRef, modelId, trx); await this.link(fieldKey, modelRef, modelId, trx);
} catch { } catch {
// Ignore catching exceptions in bulk action. // Ignore catching exceptions in bulk action.
} }

View File

@@ -1,13 +1,13 @@
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import multer from 'multer'; import { ConfigService } from '@nestjs/config';
import type { Multer } from 'multer'; import { Injectable } from '@nestjs/common';
import multerS3 from 'multer-s3';
import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi';
import config from '@/config';
@Service() @Injectable()
export class AttachmentUploadPipeline { export class AttachmentUploadPipeline {
constructor(
private readonly configService: ConfigService
) {}
/** /**
* Middleware to ensure that S3 configuration is properly set before proceeding. * Middleware to ensure that S3 configuration is properly set before proceeding.
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing. * This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
@@ -16,44 +16,21 @@ export class AttachmentUploadPipeline {
* @param next The callback to pass control to the next middleware function. * @param next The callback to pass control to the next middleware function.
*/ */
public validateS3Configured(req: Request, res: Response, next: NextFunction) { public validateS3Configured(req: Request, res: Response, next: NextFunction) {
const config = this.configService.get('s3');
if ( if (
!config.s3.region || !config.region ||
!config.s3.accessKeyId || !config.accessKeyId ||
!config.s3.secretAccessKey !config.secretAccessKey
) { ) {
const missingKeys = []; const missingKeys = [];
if (!config.s3.region) missingKeys.push('region'); if (!config.region) missingKeys.push('region');
if (!config.s3.accessKeyId) missingKeys.push('accessKeyId'); if (!config.accessKeyId) missingKeys.push('accessKeyId');
if (!config.s3.secretAccessKey) missingKeys.push('secretAccessKey'); if (!config.secretAccessKey) missingKeys.push('secretAccessKey');
const missing = missingKeys.join(', '); const missing = missingKeys.join(', ');
throw new Error(`S3 configuration error: Missing ${missing}`); throw new Error(`S3 configuration error: Missing ${missing}`);
} }
next(); next();
} }
/**
* Express middleware for uploading attachments to an S3 bucket.
* It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads.
*/
public uploadPipeline(): Multer {
return multer({
storage: multerS3({
s3,
bucket: config.s3.bucket,
contentType: multerS3.AUTO_CONTENT_TYPE,
metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname });
},
key: function (req, file, cb) {
cb(null, Date.now().toString());
},
acl: function(req, file, cb) {
// Conditionally set file to public or private based on isPublic flag
const aclValue = true ? 'public-read' : 'private';
cb(null, aclValue); // Set ACL based on the isPublic flag
}
}),
});
}
} }

View File

@@ -1,40 +1,56 @@
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import { difference } from 'lodash';
import { import {
validateLinkModelEntryExists, validateLinkModelEntryExists,
validateLinkModelExists, validateLinkModelExists,
} from './_utils'; } from './_utils';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { difference } from 'lodash'; import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common'; import { DocumentLinkModel } from './models/DocumentLink.model';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentModel } from './models/Document.model';
import { getAttachableModelsMap } from './decorators/InjectAttachable.decorator';
import { ModuleRef } from '@nestjs/core';
@Injectable() @Injectable()
export class UnlinkAttachment { export class UnlinkAttachment {
constructor(
private moduleRef: ModuleRef,
@Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
@Inject(DocumentLinkModel.name)
private readonly documentLinkModel: TenantModelProxy<
typeof DocumentLinkModel
>,
) {}
/** /**
* Unlink the attachments from the model entry. * Unlink the attachments from the model entry.
* @param {number} tenantId * @param {string} filekey - File key.
* @param {string} filekey * @param {string} modelRef - Model reference.
* @param {string} modelRef * @param {number} modelId - Model id.
* @param {number} modelId
*/ */
async unlink( async unlink(
tenantId: number,
filekey: string, filekey: string,
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
): Promise<void> { ): Promise<void> {
const { DocumentLink, Document, ...models } = this.tenancy.models(tenantId); const attachmentsAttachableModels = getAttachableModelsMap();
const attachableModel = attachmentsAttachableModels.get(modelRef);
const LinkModel = models[modelRef]; validateLinkModelExists(attachableModel);
validateLinkModelExists(LinkModel);
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
const foundLinkModel = await LinkModel.query(trx).findById(modelId); const foundLinkModel = await LinkModel.query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel); validateLinkModelEntryExists(foundLinkModel);
const document = await Document.query(trx).findOne('key', filekey); const document = await this.documentModel().query(trx).findOne('key', filekey);
// Delete the document link. // Delete the document link.
await DocumentLink.query(trx) await this.documentLinkModel().query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.where('documentId', document.id) .where('documentId', document.id)
@@ -43,22 +59,20 @@ export class UnlinkAttachment {
/** /**
* Bulk unlink the attachments from the model entry. * Bulk unlink the attachments from the model entry.
* @param {number} tenantId
* @param {string} fieldkey * @param {string} fieldkey
* @param {string} modelRef * @param {string} modelRef
* @param {number} modelId * @param {number} modelId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async bulkUnlink( async bulkUnlink(
tenantId: number,
filekeys: string[], filekeys: string[],
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
): Promise<void> { ): Promise<void> {
await bluebird.each(filekeys, (fieldKey: string) => { await bluebird.each(filekeys, (fieldKey: string) => {
try { try {
this.unlink(tenantId, fieldKey, modelRef, modelId, trx); this.unlink(fieldKey, modelRef, modelId, trx);
} catch { } catch {
// Ignore catching exceptions on bulk action. // Ignore catching exceptions on bulk action.
} }
@@ -74,15 +88,13 @@ export class UnlinkAttachment {
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx
*/ */
async unlinkUnpresentedKeys( async unlinkUnpresentedKeys(
tenantId: number,
presentedKeys: string[], presentedKeys: string[],
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
): Promise<void> { ): Promise<void> {
const { DocumentLink } = this.tenancy.models(tenantId); const modelLinks = await this.documentLinkModel()
.query(trx)
const modelLinks = await DocumentLink.query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.withGraphFetched('document'); .withGraphFetched('document');
@@ -90,33 +102,30 @@ export class UnlinkAttachment {
const modelLinkKeys = modelLinks.map((link) => link.document.key); const modelLinkKeys = modelLinks.map((link) => link.document.key);
const unpresentedKeys = difference(modelLinkKeys, presentedKeys); const unpresentedKeys = difference(modelLinkKeys, presentedKeys);
await this.bulkUnlink(tenantId, unpresentedKeys, modelRef, modelId, trx); await this.bulkUnlink(unpresentedKeys, modelRef, modelId, trx);
} }
/** /**
* Unlink all attachments of the given model type and id. * Unlink all attachments of the given model type and id.
* @param {number} tenantId
* @param {string} modelRef * @param {string} modelRef
* @param {number} modelId * @param {number} modelId
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async unlinkAllModelKeys( async unlinkAllModelKeys(
tenantId: number,
modelRef: string, modelRef: string,
modelId: number, modelId: number,
trx?: Knex.Transaction trx?: Knex.Transaction,
): Promise<void> { ): Promise<void> {
const { DocumentLink } = this.tenancy.models(tenantId);
// Get all the keys of the modelRef and modelId. // Get all the keys of the modelRef and modelId.
const modelLinks = await DocumentLink.query(trx) const modelLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.withGraphFetched('document'); .withGraphFetched('document');
const modelLinkKeys = modelLinks.map((link) => link.document.key); const modelLinkKeys = modelLinks.map((link) => link.document.key);
await this.bulkUnlink(tenantId, modelLinkKeys, modelRef, modelId, trx); await this.bulkUnlink(modelLinkKeys, modelRef, modelId, trx);
} }
} }

View File

@@ -1,24 +1,21 @@
import { castArray, difference } from 'lodash'; import { castArray, difference } from 'lodash';
import HasTenancyService from '../Tenancy/TenancyService'; import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '@/exceptions';
import { Inject, Service } from 'typedi';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentModel } from './models/Document.model'; import { DocumentModel } from './models/Document.model';
import { ServiceError } from '../Items/ServiceError';
@Service() @Injectable()
export class ValidateAttachments { export class ValidateAttachments {
constructor( constructor(
private readonly documentModel: TenantModelProxy<typeof DocumentModel> @Inject(DocumentModel.name)
) { private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
) {}
}
/** /**
* Validates the given file keys existance. * Validates the given file keys existance.
* @param {number} tenantId
* @param {string|string[]} key * @param {string|string[]} key
*/ */
async validate(tenantId: number, key: string | string[]) { async validate(key: string | string[]) {
const keys = castArray(key); const keys = castArray(key);
const documents = await this.documentModel().query().whereIn('key', key); const documents = await this.documentModel().query().whereIn('key', key);
const documentKeys = documents.map((document) => document.key); const documentKeys = documents.map((document) => document.key);

View File

@@ -1,4 +1,4 @@
import { ServiceError } from '@/exceptions'; import { ServiceError } from '../Items/ServiceError';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
export const validateLinkModelExists = (LinkModel) => { export const validateLinkModelExists = (LinkModel) => {

View File

@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
// Array to store attachable model names
const attachableModelsMap: Map<string, boolean> = new Map();
/**
* Decorator that marks a class as attachable and adds its name to the attachable models array.
* This is used to track which models can have attachments.
*/
export function InjectAttachable() {
return function (target: any) {
// Add the model name to the attachable models array
attachableModelsMap.set(target.name, true);
};
}
/**
* Get all attachable model names
*/
export function getAttachableModelsMap() {
return attachableModelsMap;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator"; import { IsNotEmpty, IsString } from "class-validator";
@@ -6,3 +7,27 @@ export class AttachmentLinkDto {
@IsNotEmpty() @IsNotEmpty()
key: string; key: string;
} }
export class UnlinkAttachmentDto {
@IsNotEmpty()
modelRef: string;
@IsNotEmpty()
modelId: number;
}
export class LinkAttachmentDto {
@IsNotEmpty()
modelRef: string;
@IsNotEmpty()
modelId: number;
}
export class UploadAttachmentDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: any;
}

View File

@@ -1,68 +1,45 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IBIllEventDeletedPayload, IBIllEventDeletedPayload,
IBillCreatedPayload, IBillCreatedPayload,
IBillCreatingPayload, IBillCreatingPayload,
IBillEditedPayload, IBillEditedPayload,
} from '@/interfaces'; } from '@/modules/Bills/Bills.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnBills { export class AttachmentsOnBills {
@Inject()
private linkAttachmentService: LinkAttachment;
@Inject()
private unlinkAttachmentService: UnlinkAttachment;
@Inject()
private validateDocuments: ValidateAttachments;
/** /**
* Constructor method. * @param {LinkAttachment} linkAttachmentService
* @param {UnlinkAttachment} unlinkAttachmentService
* @param {ValidateAttachments} validateDocuments
*/ */
public attach(bus) { constructor(
bus.subscribe( private readonly linkAttachmentService: LinkAttachment,
events.bill.onCreating, private readonly unlinkAttachmentService: UnlinkAttachment,
this.validateAttachmentsOnBillCreate.bind(this) private readonly validateDocuments: ValidateAttachments,
); ) {}
bus.subscribe(
events.bill.onCreated,
this.handleAttachmentsOnBillCreated.bind(this)
);
bus.subscribe(
events.bill.onEdited,
this.handleUnlinkUnpresentedKeysOnBillEdited.bind(this)
);
bus.subscribe(
events.bill.onEdited,
this.handleLinkPresentedKeysOnBillEdited.bind(this)
);
bus.subscribe(
events.bill.onDeleting,
this.handleUnlinkAttachmentsOnBillDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating bill. * Validates the attachment keys on creating bill.
* @param {ISaleInvoiceCreatingPaylaod} * @param {ISaleInvoiceCreatingPaylaod}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnBillCreate({ @OnEvent(events.bill.onCreating)
async validateAttachmentsOnBillCreate({
billDTO, billDTO,
tenantId,
}: IBillCreatingPayload): Promise<void> { }: IBillCreatingPayload): Promise<void> {
if (isEmpty(billDTO.attachments)) { if (isEmpty(billDTO.attachments)) {
return; return;
} }
const documentKeys = billDTO?.attachments?.map((a) => a.key); const documentKeys = billDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +47,8 @@ export class AttachmentsOnBills {
* @param {ISaleInvoiceCreatedPayload} * @param {ISaleInvoiceCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnBillCreated({ @OnEvent(events.bill.onCreated)
tenantId, async handleAttachmentsOnBillCreated({
bill, bill,
billDTO, billDTO,
trx, trx,
@@ -79,32 +56,27 @@ export class AttachmentsOnBills {
if (isEmpty(billDTO.attachments)) return; if (isEmpty(billDTO.attachments)) return;
const keys = billDTO.attachments?.map((attachment) => attachment.key); const keys = billDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink(
tenantId, await this.linkAttachmentService.bulkLink(keys, 'Bill', bill.id, trx);
keys,
'Bill',
bill.id,
trx
);
} }
/** /**
* Handles unlinking all the unpresented keys of the edited bill. * Handles unlinking all the unpresented keys of the edited bill.
* @param {IBillEditedPayload} * @param {IBillEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnBillEdited({ @OnEvent(events.bill.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnBillEdited({
billDTO, billDTO,
bill, bill,
trx trx,
}: IBillEditedPayload) { }: IBillEditedPayload) {
const keys = billDTO.attachments?.map((attachment) => attachment.key); const keys = billDTO.attachments?.map((attachment) => attachment.key);
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'Bill', 'Bill',
bill.id, bill.id,
trx trx,
); );
} }
@@ -113,8 +85,8 @@ export class AttachmentsOnBills {
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnBillEdited({ @OnEvent(events.bill.onEdited)
tenantId, async handleLinkPresentedKeysOnBillEdited({
billDTO, billDTO,
oldBill, oldBill,
trx, trx,
@@ -122,13 +94,8 @@ export class AttachmentsOnBills {
if (isEmpty(billDTO.attachments)) return; if (isEmpty(billDTO.attachments)) return;
const keys = billDTO.attachments?.map((attachment) => attachment.key); const keys = billDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink(
tenantId, await this.linkAttachmentService.bulkLink(keys, 'Bill', oldBill.id, trx);
keys,
'Bill',
oldBill.id,
trx
);
} }
/** /**
@@ -136,16 +103,15 @@ export class AttachmentsOnBills {
* @param {ISaleInvoiceDeletedPayload} * @param {ISaleInvoiceDeletedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnBillDeleted({ @OnEvent(events.bill.onDeleting)
tenantId, async handleUnlinkAttachmentsOnBillDeleted({
oldBill, oldBill,
trx, trx,
}: IBIllEventDeletedPayload) { }: IBIllEventDeletedPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'Bill', 'Bill',
oldBill.id, oldBill.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,45 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
ICreditNoteCreatedPayload, ICreditNoteCreatedPayload,
ICreditNoteCreatingPayload, ICreditNoteCreatingPayload,
ICreditNoteDeletingPayload, ICreditNoteDeletingPayload,
ICreditNoteEditedPayload, ICreditNoteEditedPayload,
} from '@/interfaces'; } from '@/modules/CreditNotes/types/CreditNotes.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { Injectable } from '@nestjs/common';
import { LinkAttachment } from '../LinkAttachment';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnCreditNote { export class AttachmentsOnCreditNote {
@Inject()
private linkAttachmentService: LinkAttachment;
@Inject()
private unlinkAttachmentService: UnlinkAttachment;
@Inject()
private validateDocuments: ValidateAttachments;
/** /**
* Constructor method. * @param {LinkAttachment} linkAttachmentService -
* @param {UnlinkAttachment} unlinkAttachmentService -
* @param {ValidateAttachments} validateDocuments -
*/ */
public attach(bus) { constructor(
bus.subscribe( private readonly linkAttachmentService: LinkAttachment,
events.creditNote.onCreating, private readonly unlinkAttachmentService: UnlinkAttachment,
this.validateAttachmentsOnCreditNoteCreate.bind(this) private readonly validateDocuments: ValidateAttachments,
); ) {}
bus.subscribe(
events.creditNote.onCreated,
this.handleAttachmentsOnCreditNoteCreated.bind(this)
);
bus.subscribe(
events.creditNote.onEdited,
this.handleUnlinkUnpresentedKeysOnCreditNoteEdited.bind(this)
);
bus.subscribe(
events.creditNote.onEdited,
this.handleLinkPresentedKeysOnCreditNoteEdited.bind(this)
);
bus.subscribe(
events.creditNote.onDeleting,
this.handleUnlinkAttachmentsOnCreditNoteDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating credit note. * Validates the attachment keys on creating credit note.
* @param {ICreditNoteCreatingPayload} * @param {ICreditNoteCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnCreditNoteCreate({ @OnEvent(events.creditNote.onCreating)
async validateAttachmentsOnCreditNoteCreate({
creditNoteDTO, creditNoteDTO,
tenantId,
}: ICreditNoteCreatingPayload): Promise<void> { }: ICreditNoteCreatingPayload): Promise<void> {
if (isEmpty(creditNoteDTO.attachments)) { if (isEmpty(creditNoteDTO.attachments)) {
return; return;
} }
const documentKeys = creditNoteDTO?.attachments?.map((a) => a.key); const documentKeys = creditNoteDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +47,8 @@ export class AttachmentsOnCreditNote {
* @param {ICreditNoteCreatedPayload} * @param {ICreditNoteCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnCreditNoteCreated({ @OnEvent(events.creditNote.onCreated)
tenantId, async handleAttachmentsOnCreditNoteCreated({
creditNote, creditNote,
creditNoteDTO, creditNoteDTO,
trx, trx,
@@ -79,12 +56,12 @@ export class AttachmentsOnCreditNote {
if (isEmpty(creditNoteDTO.attachments)) return; if (isEmpty(creditNoteDTO.attachments)) return;
const keys = creditNoteDTO.attachments?.map((attachment) => attachment.key); const keys = creditNoteDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'CreditNote', 'CreditNote',
creditNote.id, creditNote.id,
trx trx,
); );
} }
@@ -92,21 +69,20 @@ export class AttachmentsOnCreditNote {
* Handles unlinking all the unpresented keys of the edited credit note. * Handles unlinking all the unpresented keys of the edited credit note.
* @param {ICreditNoteEditedPayload} * @param {ICreditNoteEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnCreditNoteEdited({ @OnEvent(events.creditNote.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnCreditNoteEdited({
creditNoteEditDTO, creditNoteEditDTO,
oldCreditNote, oldCreditNote,
trx, trx,
}: ICreditNoteEditedPayload) { }: ICreditNoteEditedPayload) {
const keys = creditNoteEditDTO.attachments?.map( const keys = creditNoteEditDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'CreditNote', 'CreditNote',
oldCreditNote.id, oldCreditNote.id,
trx trx,
); );
} }
@@ -115,8 +91,8 @@ export class AttachmentsOnCreditNote {
* @param {ICreditNoteEditedPayload} * @param {ICreditNoteEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnCreditNoteEdited({ @OnEvent(events.creditNote.onEdited)
tenantId, async handleLinkPresentedKeysOnCreditNoteEdited({
creditNoteEditDTO, creditNoteEditDTO,
oldCreditNote, oldCreditNote,
trx, trx,
@@ -124,14 +100,13 @@ export class AttachmentsOnCreditNote {
if (isEmpty(creditNoteEditDTO.attachments)) return; if (isEmpty(creditNoteEditDTO.attachments)) return;
const keys = creditNoteEditDTO.attachments?.map( const keys = creditNoteEditDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'CreditNote', 'CreditNote',
oldCreditNote.id, oldCreditNote.id,
trx trx,
); );
} }
@@ -140,16 +115,15 @@ export class AttachmentsOnCreditNote {
* @param {ICreditNoteDeletingPayload} * @param {ICreditNoteDeletingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnCreditNoteDeleted({ @OnEvent(events.creditNote.onDeleting)
tenantId, async handleUnlinkAttachmentsOnCreditNoteDeleted({
oldCreditNote, oldCreditNote,
trx, trx,
}: ICreditNoteDeletingPayload) { }: ICreditNoteDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'CreditNote', 'CreditNote',
oldCreditNote.id, oldCreditNote.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { import {
IExpenseCreatedPayload, IExpenseCreatedPayload,
IExpenseCreatingPayload, IExpenseCreatingPayload,
IExpenseDeletingPayload, IExpenseDeletingPayload,
IExpenseEventEditPayload, IExpenseEventEditPayload,
} from '@/interfaces'; } from '@/modules/Expenses/Expenses.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnExpenses { export class AttachmentsOnExpenses {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.expenses.onCreating,
this.validateAttachmentsOnExpenseCreate.bind(this)
);
bus.subscribe(
events.expenses.onCreated,
this.handleAttachmentsOnExpenseCreated.bind(this)
);
bus.subscribe(
events.expenses.onEdited,
this.handleUnlinkUnpresentedKeysOnExpenseEdited.bind(this)
);
bus.subscribe(
events.expenses.onEdited,
this.handleLinkPresentedKeysOnExpenseEdited.bind(this)
);
bus.subscribe(
events.expenses.onDeleting,
this.handleUnlinkAttachmentsOnExpenseDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating expense. * Validates the attachment keys on creating expense.
* @param {ISaleInvoiceCreatingPaylaod} * @param {ISaleInvoiceCreatingPaylaod}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnExpenseCreate({ @OnEvent(events.expenses.onCreating)
async validateAttachmentsOnExpenseCreate({
expenseDTO, expenseDTO,
tenantId,
}: IExpenseCreatingPayload): Promise<void> { }: IExpenseCreatingPayload): Promise<void> {
if (isEmpty(expenseDTO.attachments)) { if (isEmpty(expenseDTO.attachments)) {
return; return;
} }
const documentKeys = expenseDTO?.attachments?.map((a) => a.key); const documentKeys = expenseDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnExpenses {
* @param {ISaleInvoiceCreatedPayload} * @param {ISaleInvoiceCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnExpenseCreated({ @OnEvent(events.expenses.onCreated)
tenantId, async handleAttachmentsOnExpenseCreated({
expenseDTO, expenseDTO,
expense, expense,
trx, trx,
@@ -79,12 +51,12 @@ export class AttachmentsOnExpenses {
if (isEmpty(expenseDTO.attachments)) return; if (isEmpty(expenseDTO.attachments)) return;
const keys = expenseDTO.attachments?.map((attachment) => attachment.key); const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'Expense', 'Expense',
expense.id, expense.id,
trx trx,
); );
} }
@@ -92,19 +64,18 @@ export class AttachmentsOnExpenses {
* Handles unlinking all the unpresented keys of the edited expense. * Handles unlinking all the unpresented keys of the edited expense.
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnExpenseEdited({ @OnEvent(events.expenses.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnExpenseEdited({
expenseDTO, expenseDTO,
expense, expense,
trx, trx,
}: IExpenseEventEditPayload) { }: IExpenseEventEditPayload) {
const keys = expenseDTO.attachments?.map((attachment) => attachment.key); const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'Expense', 'Expense',
expense.id, expense.id,
trx trx,
); );
} }
@@ -113,8 +84,8 @@ export class AttachmentsOnExpenses {
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnExpenseEdited({ @OnEvent(events.expenses.onEdited)
tenantId, async handleLinkPresentedKeysOnExpenseEdited({
expenseDTO, expenseDTO,
oldExpense, oldExpense,
trx, trx,
@@ -123,11 +94,10 @@ export class AttachmentsOnExpenses {
const keys = expenseDTO.attachments?.map((attachment) => attachment.key); const keys = expenseDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'Expense', 'Expense',
oldExpense.id, oldExpense.id,
trx trx,
); );
} }
@@ -136,16 +106,15 @@ export class AttachmentsOnExpenses {
* @param {ISaleInvoiceDeletedPayload} * @param {ISaleInvoiceDeletedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnExpenseDeleted({ @OnEvent(events.expenses.onDeleting)
tenantId, async handleUnlinkAttachmentsOnExpenseDeleted({
oldExpense, oldExpense,
trx, trx,
}: IExpenseDeletingPayload) { }: IExpenseDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'Expense', 'Expense',
oldExpense.id, oldExpense.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IManualJournalCreatingPayload, IManualJournalCreatingPayload,
IManualJournalEventCreatedPayload, IManualJournalEventCreatedPayload,
IManualJournalEventDeletedPayload, IManualJournalEventDeletedPayload,
IManualJournalEventEditedPayload, IManualJournalEventEditedPayload,
} from '@/interfaces'; } from '@/modules/ManualJournals/types/ManualJournals.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { OnEvent } from '@nestjs/event-emitter';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnManualJournals { export class AttachmentsOnManualJournals {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.manualJournals.onCreating,
this.validateAttachmentsOnManualJournalCreate.bind(this)
);
bus.subscribe(
events.manualJournals.onCreated,
this.handleAttachmentsOnManualJournalCreated.bind(this)
);
bus.subscribe(
events.manualJournals.onEdited,
this.handleUnlinkUnpresentedKeysOnManualJournalEdited.bind(this)
);
bus.subscribe(
events.manualJournals.onEdited,
this.handleLinkPresentedKeysOnManualJournalEdited.bind(this)
);
bus.subscribe(
events.manualJournals.onDeleting,
this.handleUnlinkAttachmentsOnManualJournalDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating manual journal. * Validates the attachment keys on creating manual journal.
* @param {IManualJournalCreatingPayload} * @param {IManualJournalCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnManualJournalCreate({ @OnEvent(events.manualJournals.onCreating)
async validateAttachmentsOnManualJournalCreate({
manualJournalDTO, manualJournalDTO,
tenantId,
}: IManualJournalCreatingPayload): Promise<void> { }: IManualJournalCreatingPayload): Promise<void> {
if (isEmpty(manualJournalDTO.attachments)) { if (isEmpty(manualJournalDTO.attachments)) {
return; return;
} }
const documentKeys = manualJournalDTO?.attachments?.map((a) => a.key); const documentKeys = manualJournalDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnManualJournals {
* @param {IManualJournalEventCreatedPayload} * @param {IManualJournalEventCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnManualJournalCreated({ @OnEvent(events.manualJournals.onCreated)
tenantId, async handleAttachmentsOnManualJournalCreated({
manualJournalDTO, manualJournalDTO,
manualJournal, manualJournal,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnManualJournals {
if (isEmpty(manualJournalDTO.attachments)) return; if (isEmpty(manualJournalDTO.attachments)) return;
const keys = manualJournalDTO.attachments?.map( const keys = manualJournalDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'ManualJournal', 'ManualJournal',
manualJournal.id, manualJournal.id,
trx trx,
); );
} }
@@ -94,21 +65,20 @@ export class AttachmentsOnManualJournals {
* Handles unlinking all the unpresented keys of the edited manual journal. * Handles unlinking all the unpresented keys of the edited manual journal.
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnManualJournalEdited({ @OnEvent(events.manualJournals.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnManualJournalEdited({
manualJournalDTO, manualJournalDTO,
manualJournal, manualJournal,
trx trx,
}: IManualJournalEventEditedPayload) { }: IManualJournalEventEditedPayload) {
const keys = manualJournalDTO.attachments?.map( const keys = manualJournalDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'SaleInvoice', 'SaleInvoice',
manualJournal.id, manualJournal.id,
trx trx,
); );
} }
@@ -117,8 +87,8 @@ export class AttachmentsOnManualJournals {
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnManualJournalEdited({ @OnEvent(events.manualJournals.onEdited)
tenantId, async handleLinkPresentedKeysOnManualJournalEdited({
manualJournalDTO, manualJournalDTO,
oldManualJournal, oldManualJournal,
trx, trx,
@@ -126,14 +96,13 @@ export class AttachmentsOnManualJournals {
if (isEmpty(manualJournalDTO.attachments)) return; if (isEmpty(manualJournalDTO.attachments)) return;
const keys = manualJournalDTO.attachments?.map( const keys = manualJournalDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'ManualJournal', 'ManualJournal',
oldManualJournal.id, oldManualJournal.id,
trx trx,
); );
} }
@@ -142,16 +111,15 @@ export class AttachmentsOnManualJournals {
* @param {ISaleInvoiceDeletedPayload} * @param {ISaleInvoiceDeletedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnManualJournalDeleted({ @OnEvent(events.manualJournals.onDeleting)
tenantId, async handleUnlinkAttachmentsOnManualJournalDeleted({
oldManualJournal, oldManualJournal,
trx, trx,
}: IManualJournalEventDeletedPayload) { }: IManualJournalEventDeletedPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleInvoice', 'SaleInvoice',
oldManualJournal.id, oldManualJournal.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IBillPaymentCreatingPayload, IBillPaymentCreatingPayload,
IBillPaymentDeletingPayload, IBillPaymentDeletingPayload,
IBillPaymentEventCreatedPayload, IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload, IBillPaymentEventEditedPayload,
} from '@/interfaces'; } from '@/modules/BillPayments/types/BillPayments.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { Injectable } from '@nestjs/common';
import { LinkAttachment } from '../LinkAttachment';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { events } from '@/common/events/events';
import { OnEvent } from '@nestjs/event-emitter';
@Service() @Injectable()
export class AttachmentsOnBillPayments { export class AttachmentsOnBillPayments {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.billPayment.onCreating,
this.validateAttachmentsOnBillPaymentCreate.bind(this)
);
bus.subscribe(
events.billPayment.onCreated,
this.handleAttachmentsOnBillPaymentCreated.bind(this)
);
bus.subscribe(
events.billPayment.onEdited,
this.handleUnlinkUnpresentedKeysOnBillPaymentEdited.bind(this)
);
bus.subscribe(
events.billPayment.onEdited,
this.handleLinkPresentedKeysOnBillPaymentEdited.bind(this)
);
bus.subscribe(
events.billPayment.onDeleting,
this.handleUnlinkAttachmentsOnBillPaymentDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating bill payment. * Validates the attachment keys on creating bill payment.
* @param {IBillPaymentCreatingPayload} * @param {IBillPaymentCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnBillPaymentCreate({ @OnEvent(events.billPayment.onCreating)
async validateAttachmentsOnBillPaymentCreate({
billPaymentDTO, billPaymentDTO,
tenantId,
}: IBillPaymentCreatingPayload): Promise<void> { }: IBillPaymentCreatingPayload): Promise<void> {
if (isEmpty(billPaymentDTO.attachments)) { if (isEmpty(billPaymentDTO.attachments)) {
return; return;
} }
const documentKeys = billPaymentDTO?.attachments?.map((a) => a.key); const documentKeys = billPaymentDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnBillPayments {
* @param {IBillPaymentEventCreatedPayload} * @param {IBillPaymentEventCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnBillPaymentCreated({ @OnEvent(events.billPayment.onCreated)
tenantId, async handleAttachmentsOnBillPaymentCreated({
billPaymentDTO, billPaymentDTO,
billPayment, billPayment,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnBillPayments {
if (isEmpty(billPaymentDTO.attachments)) return; if (isEmpty(billPaymentDTO.attachments)) return;
const keys = billPaymentDTO.attachments?.map( const keys = billPaymentDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'BillPayment', 'BillPayment',
billPayment.id, billPayment.id,
trx trx,
); );
} }
@@ -94,21 +65,20 @@ export class AttachmentsOnBillPayments {
* Handles unlinking all the unpresented keys of the edited bill payment. * Handles unlinking all the unpresented keys of the edited bill payment.
* @param {IBillPaymentEventEditedPayload} * @param {IBillPaymentEventEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnBillPaymentEdited({ @OnEvent(events.billPayment.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnBillPaymentEdited({
billPaymentDTO, billPaymentDTO,
oldBillPayment, oldBillPayment,
trx, trx,
}: IBillPaymentEventEditedPayload) { }: IBillPaymentEventEditedPayload) {
const keys = billPaymentDTO.attachments?.map( const keys = billPaymentDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'BillPayment', 'BillPayment',
oldBillPayment.id, oldBillPayment.id,
trx trx,
); );
} }
@@ -117,8 +87,8 @@ export class AttachmentsOnBillPayments {
* @param {IBillPaymentEventEditedPayload} * @param {IBillPaymentEventEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnBillPaymentEdited({ @OnEvent(events.billPayment.onEdited)
tenantId, async handleLinkPresentedKeysOnBillPaymentEdited({
billPaymentDTO, billPaymentDTO,
oldBillPayment, oldBillPayment,
trx, trx,
@@ -126,14 +96,13 @@ export class AttachmentsOnBillPayments {
if (isEmpty(billPaymentDTO.attachments)) return; if (isEmpty(billPaymentDTO.attachments)) return;
const keys = billPaymentDTO.attachments?.map( const keys = billPaymentDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'BillPayment', 'BillPayment',
oldBillPayment.id, oldBillPayment.id,
trx trx,
); );
} }
@@ -142,16 +111,15 @@ export class AttachmentsOnBillPayments {
* @param {IBillPaymentDeletingPayload} * @param {IBillPaymentDeletingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnBillPaymentDeleted({ @OnEvent(events.billPayment.onDeleting)
tenantId, async handleUnlinkAttachmentsOnBillPaymentDeleted({
oldBillPayment, oldBillPayment,
trx, trx,
}: IBillPaymentDeletingPayload) { }: IBillPaymentDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'BillPayment', 'BillPayment',
oldBillPayment.id, oldBillPayment.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IPaymentReceivedCreatedPayload, IPaymentReceivedCreatedPayload,
IPaymentReceivedCreatingPayload, IPaymentReceivedCreatingPayload,
IPaymentReceivedDeletingPayload, IPaymentReceivedDeletingPayload,
IPaymentReceivedEditedPayload, IPaymentReceivedEditedPayload,
} from '@/interfaces'; } from '@/modules/PaymentReceived/types/PaymentReceived.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnPaymentsReceived { export class AttachmentsOnPaymentsReceived {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentReceive.onCreating,
this.validateAttachmentsOnPaymentCreate.bind(this)
);
bus.subscribe(
events.paymentReceive.onCreated,
this.handleAttachmentsOnPaymentCreated.bind(this)
);
bus.subscribe(
events.paymentReceive.onEdited,
this.handleUnlinkUnpresentedKeysOnPaymentEdited.bind(this)
);
bus.subscribe(
events.paymentReceive.onEdited,
this.handleLinkPresentedKeysOnPaymentEdited.bind(this)
);
bus.subscribe(
events.paymentReceive.onDeleting,
this.handleUnlinkAttachmentsOnPaymentDelete.bind(this)
);
}
/** /**
* Validates the attachment keys on creating payment. * Validates the attachment keys on creating payment.
* @param {IPaymentReceivedCreatingPayload} * @param {IPaymentReceivedCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnPaymentCreate({ @OnEvent(events.paymentReceive.onCreating)
async validateAttachmentsOnPaymentCreate({
paymentReceiveDTO, paymentReceiveDTO,
tenantId,
}: IPaymentReceivedCreatingPayload): Promise<void> { }: IPaymentReceivedCreatingPayload): Promise<void> {
if (isEmpty(paymentReceiveDTO.attachments)) { if (isEmpty(paymentReceiveDTO.attachments)) {
return; return;
} }
const documentKeys = paymentReceiveDTO?.attachments?.map((a) => a.key); const documentKeys = paymentReceiveDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnPaymentsReceived {
* @param {IPaymentReceivedCreatedPayload} * @param {IPaymentReceivedCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnPaymentCreated({ @OnEvent(events.paymentReceive.onCreated)
tenantId, async handleAttachmentsOnPaymentCreated({
paymentReceiveDTO, paymentReceiveDTO,
paymentReceive, paymentReceive,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnPaymentsReceived {
if (isEmpty(paymentReceiveDTO.attachments)) return; if (isEmpty(paymentReceiveDTO.attachments)) return;
const keys = paymentReceiveDTO.attachments?.map( const keys = paymentReceiveDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'PaymentReceive', 'PaymentReceive',
paymentReceive.id, paymentReceive.id,
trx trx,
); );
} }
@@ -94,21 +65,20 @@ export class AttachmentsOnPaymentsReceived {
* Handles unlinking all the unpresented keys of the edited payment. * Handles unlinking all the unpresented keys of the edited payment.
* @param {IPaymentReceivedEditedPayload} * @param {IPaymentReceivedEditedPayload}
*/ */
@OnEvent(events.paymentReceive.onEdited)
private async handleUnlinkUnpresentedKeysOnPaymentEdited({ private async handleUnlinkUnpresentedKeysOnPaymentEdited({
tenantId,
paymentReceiveDTO, paymentReceiveDTO,
oldPaymentReceive, oldPaymentReceive,
trx, trx,
}: IPaymentReceivedEditedPayload) { }: IPaymentReceivedEditedPayload) {
const keys = paymentReceiveDTO.attachments?.map( const keys = paymentReceiveDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'PaymentReceive', 'PaymentReceive',
oldPaymentReceive.id, oldPaymentReceive.id,
trx trx,
); );
} }
@@ -117,8 +87,8 @@ export class AttachmentsOnPaymentsReceived {
* @param {IPaymentReceivedEditedPayload} * @param {IPaymentReceivedEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnPaymentEdited({ @OnEvent(events.paymentReceive.onEdited)
tenantId, async handleLinkPresentedKeysOnPaymentEdited({
paymentReceiveDTO, paymentReceiveDTO,
oldPaymentReceive, oldPaymentReceive,
trx, trx,
@@ -126,14 +96,13 @@ export class AttachmentsOnPaymentsReceived {
if (isEmpty(paymentReceiveDTO.attachments)) return; if (isEmpty(paymentReceiveDTO.attachments)) return;
const keys = paymentReceiveDTO.attachments?.map( const keys = paymentReceiveDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'PaymentReceive', 'PaymentReceive',
oldPaymentReceive.id, oldPaymentReceive.id,
trx trx,
); );
} }
@@ -142,16 +111,15 @@ export class AttachmentsOnPaymentsReceived {
* @param {ISaleInvoiceDeletedPayload} * @param {ISaleInvoiceDeletedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnPaymentDelete({ @OnEvent(events.paymentReceive.onDeleting)
tenantId, async handleUnlinkAttachmentsOnPaymentDelete({
oldPaymentReceive, oldPaymentReceive,
trx, trx,
}: IPaymentReceivedDeletingPayload) { }: IPaymentReceivedDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'PaymentReceive', 'PaymentReceive',
oldPaymentReceive.id, oldPaymentReceive.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
ISaleEstimateCreatedPayload, ISaleEstimateCreatedPayload,
ISaleEstimateCreatingPayload, ISaleEstimateCreatingPayload,
ISaleEstimateDeletingPayload, ISaleEstimateDeletingPayload,
ISaleEstimateEditedPayload, ISaleEstimateEditedPayload,
} from '@/interfaces'; } from '@/modules/SaleEstimates/types/SaleEstimates.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { Injectable } from '@nestjs/common';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnSaleEstimates { export class AttachmentsOnSaleEstimates {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleEstimate.onCreating,
this.validateAttachmentsOnSaleEstimateCreated.bind(this)
);
bus.subscribe(
events.saleEstimate.onCreated,
this.handleAttachmentsOnSaleEstimateCreated.bind(this)
);
bus.subscribe(
events.saleEstimate.onEdited,
this.handleUnlinkUnpresentedKeysOnSaleEstimateEdited.bind(this)
);
bus.subscribe(
events.saleEstimate.onEdited,
this.handleLinkPresentedKeysOnSaleEstimateEdited.bind(this)
);
bus.subscribe(
events.saleEstimate.onDeleting,
this.handleUnlinkAttachmentsOnSaleEstimateDelete.bind(this)
);
}
/** /**
* Validates the attachment keys on creating sale estimate. * Validates the attachment keys on creating sale estimate.
* @param {ISaleEstimateCreatingPayload} * @param {ISaleEstimateCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnSaleEstimateCreated({ @OnEvent(events.saleEstimate.onCreating)
async validateAttachmentsOnSaleEstimateCreated({
estimateDTO, estimateDTO,
tenantId,
}: ISaleEstimateCreatingPayload): Promise<void> { }: ISaleEstimateCreatingPayload): Promise<void> {
if (isEmpty(estimateDTO.attachments)) { if (isEmpty(estimateDTO.attachments)) {
return; return;
} }
const documentKeys = estimateDTO?.attachments?.map((a) => a.key); const documentKeys = estimateDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnSaleEstimates {
* @param {ISaleEstimateCreatedPayload} * @param {ISaleEstimateCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnSaleEstimateCreated({ @OnEvent(events.saleEstimate.onCreated)
tenantId, async handleAttachmentsOnSaleEstimateCreated({
saleEstimateDTO, saleEstimateDTO,
saleEstimate, saleEstimate,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnSaleEstimates {
if (isEmpty(saleEstimateDTO.attachments)) return; if (isEmpty(saleEstimateDTO.attachments)) return;
const keys = saleEstimateDTO.attachments?.map( const keys = saleEstimateDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleEstimate', 'SaleEstimate',
saleEstimate.id, saleEstimate.id,
trx trx,
); );
} }
@@ -94,20 +65,19 @@ export class AttachmentsOnSaleEstimates {
* Handles unlinking all the unpresented keys of the edited sale estimate. * Handles unlinking all the unpresented keys of the edited sale estimate.
* @param {ISaleEstimateEditedPayload} * @param {ISaleEstimateEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnSaleEstimateEdited({ @OnEvent(events.saleEstimate.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnSaleEstimateEdited({
estimateDTO, estimateDTO,
oldSaleEstimate, oldSaleEstimate,
trx trx,
}: ISaleEstimateEditedPayload) { }: ISaleEstimateEditedPayload) {
const keys = estimateDTO.attachments?.map((attachment) => attachment.key); const keys = estimateDTO.attachments?.map((attachment) => attachment.key);
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'SaleEstimate', 'SaleEstimate',
oldSaleEstimate.id, oldSaleEstimate.id,
trx trx,
); );
} }
@@ -116,8 +86,8 @@ export class AttachmentsOnSaleEstimates {
* @param {ISaleEstimateEditedPayload} * @param {ISaleEstimateEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnSaleEstimateEdited({ @OnEvent(events.saleEstimate.onEdited)
tenantId, async handleLinkPresentedKeysOnSaleEstimateEdited({
estimateDTO, estimateDTO,
oldSaleEstimate, oldSaleEstimate,
trx, trx,
@@ -125,12 +95,12 @@ export class AttachmentsOnSaleEstimates {
if (isEmpty(estimateDTO.attachments)) return; if (isEmpty(estimateDTO.attachments)) return;
const keys = estimateDTO.attachments?.map((attachment) => attachment.key); const keys = estimateDTO.attachments?.map((attachment) => attachment.key);
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleEstimate', 'SaleEstimate',
oldSaleEstimate.id, oldSaleEstimate.id,
trx trx,
); );
} }
@@ -139,16 +109,15 @@ export class AttachmentsOnSaleEstimates {
* @param {ISaleEstimateDeletingPayload} * @param {ISaleEstimateDeletingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnSaleEstimateDelete({ @OnEvent(events.saleEstimate.onDeleting)
tenantId, async handleUnlinkAttachmentsOnSaleEstimateDelete({
oldSaleEstimate, oldSaleEstimate,
trx, trx,
}: ISaleEstimateDeletingPayload) { }: ISaleEstimateDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleEstimate', 'SaleEstimate',
oldSaleEstimate.id, oldSaleEstimate.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi'; import { Injectable } from '@nestjs/common';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { OnEvent } from '@nestjs/event-emitter';
import { import {
ISaleInvoiceCreatedPayload, ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod, ISaleInvoiceCreatingPaylaod,
ISaleInvoiceDeletingPayload, ISaleInvoiceDeletingPayload,
ISaleInvoiceEditedPayload, ISaleInvoiceEditedPayload,
} from '@/interfaces'; } from '@/modules/SaleInvoices/SaleInvoice.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { LinkAttachment } from '../LinkAttachment';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnSaleInvoiceCreated { export class AttachmentsOnSaleInvoiceCreated {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreating,
this.validateAttachmentsOnSaleInvoiceCreate.bind(this)
);
bus.subscribe(
events.saleInvoice.onCreated,
this.handleAttachmentsOnSaleInvoiceCreated.bind(this)
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleUnlinkUnpresentedKeysOnInvoiceEdited.bind(this)
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleLinkPresentedKeysOnInvoiceEdited.bind(this)
);
bus.subscribe(
events.saleInvoice.onDeleting,
this.handleUnlinkAttachmentsOnInvoiceDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating sale invoice. * Validates the attachment keys on creating sale invoice.
* @param {ISaleInvoiceCreatingPaylaod} * @param {ISaleInvoiceCreatingPaylaod}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnSaleInvoiceCreate({ @OnEvent(events.saleInvoice.onCreating)
async validateAttachmentsOnSaleInvoiceCreate({
saleInvoiceDTO, saleInvoiceDTO,
tenantId,
}: ISaleInvoiceCreatingPaylaod): Promise<void> { }: ISaleInvoiceCreatingPaylaod): Promise<void> {
if (isEmpty(saleInvoiceDTO.attachments)) { if (isEmpty(saleInvoiceDTO.attachments)) {
return; return;
} }
const documentKeys = saleInvoiceDTO?.attachments?.map((a) => a.key); const documentKeys = saleInvoiceDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnSaleInvoiceCreated {
* @param {ISaleInvoiceCreatedPayload} * @param {ISaleInvoiceCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnSaleInvoiceCreated({ @OnEvent(events.saleInvoice.onCreated)
tenantId, async handleAttachmentsOnSaleInvoiceCreated({
saleInvoiceDTO, saleInvoiceDTO,
saleInvoice, saleInvoice,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnSaleInvoiceCreated {
if (isEmpty(saleInvoiceDTO.attachments)) return; if (isEmpty(saleInvoiceDTO.attachments)) return;
const keys = saleInvoiceDTO.attachments?.map( const keys = saleInvoiceDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleInvoice', 'SaleInvoice',
saleInvoice.id, saleInvoice.id,
trx trx,
); );
} }
@@ -94,8 +65,8 @@ export class AttachmentsOnSaleInvoiceCreated {
* Handles unlinking all the unpresented keys of the edited sale invoice. * Handles unlinking all the unpresented keys of the edited sale invoice.
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnInvoiceEdited({ @OnEvent(events.saleInvoice.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnInvoiceEdited({
saleInvoiceDTO, saleInvoiceDTO,
saleInvoice, saleInvoice,
trx, trx,
@@ -103,14 +74,13 @@ export class AttachmentsOnSaleInvoiceCreated {
// if (isEmpty(saleInvoiceDTO.attachments)) return; // if (isEmpty(saleInvoiceDTO.attachments)) return;
const keys = saleInvoiceDTO.attachments?.map( const keys = saleInvoiceDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'SaleInvoice', 'SaleInvoice',
saleInvoice.id, saleInvoice.id,
trx trx,
); );
} }
@@ -119,8 +89,8 @@ export class AttachmentsOnSaleInvoiceCreated {
* @param {ISaleInvoiceEditedPayload} * @param {ISaleInvoiceEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnInvoiceEdited({ @OnEvent(events.saleInvoice.onEdited)
tenantId, async handleLinkPresentedKeysOnInvoiceEdited({
saleInvoiceDTO, saleInvoiceDTO,
oldSaleInvoice, oldSaleInvoice,
trx, trx,
@@ -128,14 +98,13 @@ export class AttachmentsOnSaleInvoiceCreated {
if (isEmpty(saleInvoiceDTO.attachments)) return; if (isEmpty(saleInvoiceDTO.attachments)) return;
const keys = saleInvoiceDTO.attachments?.map( const keys = saleInvoiceDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleInvoice', 'SaleInvoice',
oldSaleInvoice.id, oldSaleInvoice.id,
trx trx,
); );
} }
@@ -144,16 +113,15 @@ export class AttachmentsOnSaleInvoiceCreated {
* @param {ISaleInvoiceDeletedPayload} * @param {ISaleInvoiceDeletedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnInvoiceDeleted({ @OnEvent(events.saleInvoice.onDeleting)
tenantId, async handleUnlinkAttachmentsOnInvoiceDeleted({
oldSaleInvoice, oldSaleInvoice,
trx, trx,
}: ISaleInvoiceDeletingPayload) { }: ISaleInvoiceDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleInvoice', 'SaleInvoice',
oldSaleInvoice.id, oldSaleInvoice.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { import {
ISaleReceiptCreatedPayload, ISaleReceiptCreatedPayload,
ISaleReceiptCreatingPayload, ISaleReceiptCreatingPayload,
ISaleReceiptDeletingPayload, ISaleReceiptDeletingPayload,
ISaleReceiptEditedPayload, ISaleReceiptEditedPayload,
} from '@/interfaces'; } from '../../SaleReceipts/types/SaleReceipts.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { LinkAttachment } from '../LinkAttachment';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnSaleReceipt { export class AttachmentsOnSaleReceipt {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleReceipt.onCreating,
this.validateAttachmentsOnSaleInvoiceCreate.bind(this)
);
bus.subscribe(
events.saleReceipt.onCreated,
this.handleAttachmentsOnSaleInvoiceCreated.bind(this)
);
bus.subscribe(
events.saleReceipt.onEdited,
this.handleUnlinkUnpresentedKeysOnInvoiceEdited.bind(this)
);
bus.subscribe(
events.saleReceipt.onEdited,
this.handleLinkPresentedKeysOnInvoiceEdited.bind(this)
);
bus.subscribe(
events.saleReceipt.onDeleting,
this.handleUnlinkAttachmentsOnReceiptDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating sale receipt. * Validates the attachment keys on creating sale receipt.
* @param {ISaleReceiptCreatingPayload} * @param {ISaleReceiptCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnSaleInvoiceCreate({ @OnEvent(events.saleReceipt.onCreating)
async validateAttachmentsOnSaleInvoiceCreate({
saleReceiptDTO, saleReceiptDTO,
tenantId,
}: ISaleReceiptCreatingPayload): Promise<void> { }: ISaleReceiptCreatingPayload): Promise<void> {
if (isEmpty(saleReceiptDTO.attachments)) { if (isEmpty(saleReceiptDTO.attachments)) {
return; return;
} }
const documentKeys = saleReceiptDTO?.attachments?.map((a) => a.key); const documentKeys = saleReceiptDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnSaleReceipt {
* @param {ISaleReceiptCreatedPayload} * @param {ISaleReceiptCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnSaleInvoiceCreated({ @OnEvent(events.saleReceipt.onCreated)
tenantId, async handleAttachmentsOnSaleInvoiceCreated({
saleReceiptDTO, saleReceiptDTO,
saleReceipt, saleReceipt,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnSaleReceipt {
if (isEmpty(saleReceiptDTO.attachments)) return; if (isEmpty(saleReceiptDTO.attachments)) return;
const keys = saleReceiptDTO.attachments?.map( const keys = saleReceiptDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleReceipt', 'SaleReceipt',
saleReceipt.id, saleReceipt.id,
trx trx,
); );
} }
@@ -94,21 +65,20 @@ export class AttachmentsOnSaleReceipt {
* Handles unlinking all the unpresented keys of the edited sale receipt. * Handles unlinking all the unpresented keys of the edited sale receipt.
* @param {ISaleReceiptEditedPayload} * @param {ISaleReceiptEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnInvoiceEdited({ @OnEvent(events.saleReceipt.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnInvoiceEdited({
saleReceiptDTO, saleReceiptDTO,
saleReceipt, saleReceipt,
trx, trx,
}: ISaleReceiptEditedPayload) { }: ISaleReceiptEditedPayload) {
const keys = saleReceiptDTO.attachments?.map( const keys = saleReceiptDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'SaleReceipt', 'SaleReceipt',
saleReceipt.id, saleReceipt.id,
trx trx,
); );
} }
@@ -117,8 +87,8 @@ export class AttachmentsOnSaleReceipt {
* @param {ISaleReceiptEditedPayload} * @param {ISaleReceiptEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnInvoiceEdited({ @OnEvent(events.saleReceipt.onEdited)
tenantId, async handleLinkPresentedKeysOnInvoiceEdited({
saleReceiptDTO, saleReceiptDTO,
oldSaleReceipt, oldSaleReceipt,
trx, trx,
@@ -126,14 +96,13 @@ export class AttachmentsOnSaleReceipt {
if (isEmpty(saleReceiptDTO.attachments)) return; if (isEmpty(saleReceiptDTO.attachments)) return;
const keys = saleReceiptDTO.attachments?.map( const keys = saleReceiptDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'SaleReceipt', 'SaleReceipt',
oldSaleReceipt.id, oldSaleReceipt.id,
trx trx,
); );
} }
@@ -142,16 +111,15 @@ export class AttachmentsOnSaleReceipt {
* @param {ISaleReceiptDeletingPayload} * @param {ISaleReceiptDeletingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnReceiptDeleted({ @OnEvent(events.saleReceipt.onDeleting)
tenantId, async handleUnlinkAttachmentsOnReceiptDeleted({
oldSaleReceipt, oldSaleReceipt,
trx, trx,
}: ISaleReceiptDeletingPayload) { }: ISaleReceiptDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleReceipt', 'SaleReceipt',
oldSaleReceipt.id, oldSaleReceipt.id,
trx trx,
); );
} }
} }

View File

@@ -1,68 +1,40 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IVendorCreditCreatedPayload, IVendorCreditCreatedPayload,
IVendorCreditCreatingPayload, IVendorCreditCreatingPayload,
IVendorCreditDeletingPayload, IVendorCreditDeletingPayload,
IVendorCreditEditedPayload, IVendorCreditEditedPayload,
} from '@/interfaces'; } from '../../VendorCredit/types/VendorCredit.types';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
import { ValidateAttachments } from '../ValidateAttachments'; import { ValidateAttachments } from '../ValidateAttachments';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { UnlinkAttachment } from '../UnlinkAttachment'; import { UnlinkAttachment } from '../UnlinkAttachment';
import { LinkAttachment } from '../LinkAttachment';
import { events } from '@/common/events/events';
@Service() @Injectable()
export class AttachmentsOnVendorCredits { export class AttachmentsOnVendorCredits {
@Inject() constructor(
private linkAttachmentService: LinkAttachment; private readonly linkAttachmentService: LinkAttachment,
private readonly unlinkAttachmentService: UnlinkAttachment,
@Inject() private readonly validateDocuments: ValidateAttachments,
private unlinkAttachmentService: UnlinkAttachment; ) {}
@Inject()
private validateDocuments: ValidateAttachments;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.vendorCredit.onCreating,
this.validateAttachmentsOnVendorCreditCreate.bind(this)
);
bus.subscribe(
events.vendorCredit.onCreated,
this.handleAttachmentsOnVendorCreditCreated.bind(this)
);
bus.subscribe(
events.vendorCredit.onEdited,
this.handleUnlinkUnpresentedKeysOnVendorCreditEdited.bind(this)
);
bus.subscribe(
events.vendorCredit.onEdited,
this.handleLinkPresentedKeysOnVendorCreditEdited.bind(this)
);
bus.subscribe(
events.vendorCredit.onDeleting,
this.handleUnlinkAttachmentsOnVendorCreditDeleted.bind(this)
);
}
/** /**
* Validates the attachment keys on creating vendor credit. * Validates the attachment keys on creating vendor credit.
* @param {IVendorCreditCreatingPayload} * @param {IVendorCreditCreatingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async validateAttachmentsOnVendorCreditCreate({ @OnEvent(events.vendorCredit.onCreating)
async validateAttachmentsOnVendorCreditCreate({
vendorCreditCreateDTO, vendorCreditCreateDTO,
tenantId,
}: IVendorCreditCreatingPayload): Promise<void> { }: IVendorCreditCreatingPayload): Promise<void> {
if (isEmpty(vendorCreditCreateDTO.attachments)) { if (isEmpty(vendorCreditCreateDTO.attachments)) {
return; return;
} }
const documentKeys = vendorCreditCreateDTO?.attachments?.map((a) => a.key); const documentKeys = vendorCreditCreateDTO?.attachments?.map((a) => a.key);
await this.validateDocuments.validate(tenantId, documentKeys); await this.validateDocuments.validate(documentKeys);
} }
/** /**
@@ -70,8 +42,8 @@ export class AttachmentsOnVendorCredits {
* @param {IVendorCreditCreatedPayload} * @param {IVendorCreditCreatedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleAttachmentsOnVendorCreditCreated({ @OnEvent(events.vendorCredit.onCreated)
tenantId, async handleAttachmentsOnVendorCreditCreated({
vendorCreditCreateDTO, vendorCreditCreateDTO,
vendorCredit, vendorCredit,
trx, trx,
@@ -79,14 +51,13 @@ export class AttachmentsOnVendorCredits {
if (isEmpty(vendorCreditCreateDTO.attachments)) return; if (isEmpty(vendorCreditCreateDTO.attachments)) return;
const keys = vendorCreditCreateDTO.attachments?.map( const keys = vendorCreditCreateDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'VendorCredit', 'VendorCredit',
vendorCredit.id, vendorCredit.id,
trx trx,
); );
} }
@@ -94,21 +65,20 @@ export class AttachmentsOnVendorCredits {
* Handles unlinking all the unpresented keys of the edited vendor credit. * Handles unlinking all the unpresented keys of the edited vendor credit.
* @param {IVendorCreditEditedPayload} * @param {IVendorCreditEditedPayload}
*/ */
private async handleUnlinkUnpresentedKeysOnVendorCreditEdited({ @OnEvent(events.vendorCredit.onEdited)
tenantId, async handleUnlinkUnpresentedKeysOnVendorCreditEdited({
vendorCreditDTO, vendorCreditDTO,
oldVendorCredit, oldVendorCredit,
trx, trx,
}: IVendorCreditEditedPayload) { }: IVendorCreditEditedPayload) {
const keys = vendorCreditDTO.attachments?.map( const keys = vendorCreditDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.unlinkAttachmentService.unlinkUnpresentedKeys( await this.unlinkAttachmentService.unlinkUnpresentedKeys(
tenantId,
keys, keys,
'VendorCredit', 'VendorCredit',
oldVendorCredit.id, oldVendorCredit.id,
trx trx,
); );
} }
@@ -117,8 +87,8 @@ export class AttachmentsOnVendorCredits {
* @param {IVendorCreditEditedPayload} * @param {IVendorCreditEditedPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleLinkPresentedKeysOnVendorCreditEdited({ @OnEvent(events.vendorCredit.onEdited)
tenantId, async handleLinkPresentedKeysOnVendorCreditEdited({
vendorCreditDTO, vendorCreditDTO,
oldVendorCredit, oldVendorCredit,
trx, trx,
@@ -126,14 +96,13 @@ export class AttachmentsOnVendorCredits {
if (isEmpty(vendorCreditDTO.attachments)) return; if (isEmpty(vendorCreditDTO.attachments)) return;
const keys = vendorCreditDTO.attachments?.map( const keys = vendorCreditDTO.attachments?.map(
(attachment) => attachment.key (attachment) => attachment.key,
); );
await this.linkAttachmentService.bulkLink( await this.linkAttachmentService.bulkLink(
tenantId,
keys, keys,
'VendorCredit', 'VendorCredit',
oldVendorCredit.id, oldVendorCredit.id,
trx trx,
); );
} }
@@ -142,16 +111,15 @@ export class AttachmentsOnVendorCredits {
* @param {IVendorCreditDeletingPayload} * @param {IVendorCreditDeletingPayload}
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private async handleUnlinkAttachmentsOnVendorCreditDeleted({ @OnEvent(events.vendorCredit.onDeleting)
tenantId, async handleUnlinkAttachmentsOnVendorCreditDeleted({
oldVendorCredit, oldVendorCredit,
trx, trx,
}: IVendorCreditDeletingPayload) { }: IVendorCreditDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys( await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'VendorCredit', 'VendorCredit',
oldVendorCredit.id, oldVendorCredit.id,
trx trx,
); );
} }
} }

View File

@@ -1,7 +1,9 @@
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { Model, mixin } from 'objection'; import { Model, mixin } from 'objection';
import { DocumentModel } from './Document.model';
export class DocumentLinkModel extends TenantBaseModel { export class DocumentLinkModel extends TenantBaseModel {
document!: DocumentModel;
/** /**
* Table name * Table name
*/ */

View File

@@ -1,9 +1,10 @@
import path from 'path'; import path from 'path';
import config from '@/config'; // import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => { export const getUploadedObjectUri = (objectKey: string) => {
return new URL( return '';
path.join(config.s3.bucket, objectKey), // return new URL(
config.s3.endpoint // path.join(config.s3.bucket, objectKey),
).toString(); // config.s3.endpoint
// ).toString();
}; };

View File

@@ -6,7 +6,7 @@ import { BaseModel } from '@/models/Model';
export class DocumentLink extends BaseModel{ export class DocumentLink extends BaseModel{
public modelRef: string; public modelRef: string;
public modelId: string; public modelId: number;
public documentId: number; public documentId: number;
public expiresAt?: Date; public expiresAt?: Date;

View File

@@ -4,11 +4,6 @@ import * as moment from 'moment';
import * as composeAsync from 'async/compose'; import * as composeAsync from 'async/compose';
import * as R from 'ramda'; import * as R from 'ramda';
import { ERRORS } from '../constants'; import { ERRORS } from '../constants';
import {
ICreditNoteEditDTO,
ICreditNoteEntryNewDTO,
ICreditNoteNewDTO,
} from '../types/CreditNotes.types';
import { ServiceError } from '@/modules/Items/ServiceError'; import { ServiceError } from '@/modules/Items/ServiceError';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';

View File

@@ -1,43 +1,9 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { CreditNote } from '../models/CreditNote'; import { CreditNote } from '../models/CreditNote';
import { RefundCreditNote } from '../../CreditNoteRefunds/models/RefundCreditNote';
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
import { IItemEntryDTO } from '@/modules/TransactionItemEntry/ItemEntry.types';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { EditCreditNoteDto } from '../dtos/CreditNote.dto'; import { CreateCreditNoteDto, EditCreditNoteDto } from '../dtos/CreditNote.dto';
export interface ICreditNoteEntryNewDTO extends IItemEntryDTO {}
export interface ICreditNoteNewDTO {
customerId: number;
exchangeRate?: number;
creditNoteDate: Date;
creditNoteNumber: string;
note: string;
open: boolean;
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
// discountType?: DiscountType;
adjustment?: number;
}
export interface ICreditNoteEditDTO {
customerId: number;
exchangeRate?: number;
creditNoteDate: Date;
creditNoteNumber: string;
note: string;
open: boolean;
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
}
export enum CreditNoteAction { export enum CreditNoteAction {
Create = 'Create', Create = 'Create',
@@ -74,13 +40,13 @@ export interface ICreditNoteEditedPayload {
} }
export interface ICreditNoteCreatedPayload { export interface ICreditNoteCreatedPayload {
creditNoteDTO: ICreditNoteNewDTO; creditNoteDTO: CreateCreditNoteDto;
creditNote: CreditNote; creditNote: CreditNote;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICreditNoteCreatingPayload { export interface ICreditNoteCreatingPayload {
creditNoteDTO: ICreditNoteNewDTO; creditNoteDTO: CreateCreditNoteDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }

View File

@@ -2,6 +2,8 @@ import { Knex } from 'knex';
import { Expense } from './models/Expense.model'; import { Expense } from './models/Expense.model';
import { SystemUser } from '../System/models/SystemUser'; import { SystemUser } from '../System/models/SystemUser';
import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types'; import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
import { CreateExpense } from './commands/CreateExpense.service';
export interface IPaginationMeta { export interface IPaginationMeta {
total: number; total: number;
@@ -19,59 +21,28 @@ export interface IExpensesFilter {
filterQuery?: (query: any) => void; filterQuery?: (query: any) => void;
} }
export interface IExpenseCommonDTO {
currencyCode: string;
exchangeRate?: number;
description?: string;
paymentAccountId: number;
peyeeId?: number;
referenceNo?: string;
publish: boolean;
userId: number;
paymentDate: Date;
payeeId: number;
categories: IExpenseCategoryDTO[];
branchId?: number;
// attachments?: AttachmentLinkDTO[];
}
export interface IExpenseCreateDTO extends IExpenseCommonDTO {}
export interface IExpenseEditDTO extends IExpenseCommonDTO {}
export interface IExpenseCategoryDTO {
id?: number;
expenseAccountId: number;
index: number;
amount: number;
description?: string;
expenseId: number;
landedCost?: boolean;
projectId?: number;
}
export interface IExpenseCreatingPayload { export interface IExpenseCreatingPayload {
trx: Knex.Transaction; trx: Knex.Transaction;
expenseDTO: IExpenseCreateDTO; expenseDTO: CreateExpenseDto;
} }
export interface IExpenseEventEditingPayload { export interface IExpenseEventEditingPayload {
oldExpense: Expense; oldExpense: Expense;
expenseDTO: IExpenseEditDTO; expenseDTO: EditExpenseDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface IExpenseCreatedPayload { export interface IExpenseCreatedPayload {
expenseId: number; expenseId: number;
expense: Expense; expense: Expense;
expenseDTO: IExpenseCreateDTO; expenseDTO: CreateExpenseDto;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
export interface IExpenseEventEditPayload { export interface IExpenseEventEditPayload {
expenseId: number; expenseId: number;
expense: Expense; expense: Expense;
expenseDTO: IExpenseEditDTO; expenseDTO: EditExpenseDto;
authorizedUser: SystemUser; authorizedUser: SystemUser;
oldExpense: Expense; oldExpense: Expense;
trx: Knex.Transaction; trx: Knex.Transaction;

View File

@@ -7,6 +7,7 @@ export const S3_CLIENT = 'S3_CLIENT';
const services = [ const services = [
{ {
provide: S3_CLIENT, provide: S3_CLIENT,
inject: [ConfigService],
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
const config = configService.get('s3'); const config = configService.get('s3');

View File

@@ -29,6 +29,12 @@ class PaymentMethodDto {
enable: boolean; enable: boolean;
} }
class AttachmentDto {
@IsString()
key: string;
}
class CommandSaleInvoiceDto { class CommandSaleInvoiceDto {
@IsInt() @IsInt()
@IsNotEmpty() @IsNotEmpty()
@@ -186,6 +192,16 @@ class CommandSaleInvoiceDto {
example: 1, example: 1,
}) })
fromEstimateId?: number; fromEstimateId?: number;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
@ApiProperty({
description: 'The attachments of the sale receipt',
example: [{ key: '123456' }],
})
attachments?: AttachmentDto[];
} }
export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {} export class CreateSaleInvoiceDto extends CommandSaleInvoiceDto {}

View File

@@ -13,7 +13,9 @@ import { ISearchRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilte
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types'; import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types';
import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model'; import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
export class SaleInvoice extends TenantBaseModel{ export class SaleInvoice extends TenantBaseModel{
public taxAmountWithheld: number; public taxAmountWithheld: number;
public balance: number; public balance: number;

View File

@@ -3,7 +3,6 @@ import { Knex } from 'knex';
import { import {
ISaleReceiptCreatedPayload, ISaleReceiptCreatedPayload,
ISaleReceiptCreatingPayload, ISaleReceiptCreatingPayload,
ISaleReceiptDTO,
} from '../types/SaleReceipts.types'; } from '../types/SaleReceipts.types';
import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service'; import { SaleReceiptDTOTransformer } from './SaleReceiptDTOTransformer.service';
import { SaleReceiptValidators } from './SaleReceiptValidators.service'; import { SaleReceiptValidators } from './SaleReceiptValidators.service';

View File

@@ -1,39 +1,21 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
// import { IItemEntry } from './ItemEntry';
// import { CommonMailOptions, CommonMailOptionsDTO } from '../SaleInvoices/types/Mailable';
import { AttachmentLinkDTO } from '../../Attachments/Attachments.types';
import { SaleReceipt } from '../models/SaleReceipt'; import { SaleReceipt } from '../models/SaleReceipt';
import { CommonMailOptionsDTO } from '@/modules/MailNotification/MailNotification.types'; import { CommonMailOptionsDTO } from '@/modules/MailNotification/MailNotification.types';
import { CommonMailOptions } from '@/modules/MailNotification/MailNotification.types'; import { CommonMailOptions } from '@/modules/MailNotification/MailNotification.types';
import { TenantJobPayload } from '@/interfaces/Tenant'; import { TenantJobPayload } from '@/interfaces/Tenant';
import { CreateSaleReceiptDto, EditSaleReceiptDto } from '../dtos/SaleReceipt.dto';
export interface ISalesReceiptsFilter { export interface ISalesReceiptsFilter {
filterQuery?: (query: any) => void; filterQuery?: (query: any) => void;
} }
export interface ISaleReceiptDTO {
customerId: number;
exchangeRate?: number;
depositAccountId: number;
receiptDate: Date;
sendToEmail: string;
referenceNo?: string;
receiptNumber?: string;
receiptMessage: string;
statement: string;
closed: boolean;
entries: any[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface ISaleReceiptSmsDetails { export interface ISaleReceiptSmsDetails {
customerName: string; customerName: string;
customerPhoneNumber: string; customerPhoneNumber: string;
smsMessage: string; smsMessage: string;
} }
export interface ISaleReceiptCreatingPayload { export interface ISaleReceiptCreatingPayload {
saleReceiptDTO: ISaleReceiptDTO; saleReceiptDTO: CreateSaleReceiptDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
@@ -41,21 +23,20 @@ export interface ISaleReceiptCreatedPayload {
// tenantId: number; // tenantId: number;
saleReceipt: SaleReceipt; saleReceipt: SaleReceipt;
saleReceiptId: number; saleReceiptId: number;
saleReceiptDTO: ISaleReceiptDTO; saleReceiptDTO: CreateSaleReceiptDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ISaleReceiptEditedPayload { export interface ISaleReceiptEditedPayload {
oldSaleReceipt: SaleReceipt; oldSaleReceipt: SaleReceipt;
saleReceipt: SaleReceipt; saleReceipt: SaleReceipt;
// saleReceiptId: number; saleReceiptDTO: EditSaleReceiptDto;
saleReceiptDTO: ISaleReceiptDTO;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ISaleReceiptEditingPayload { export interface ISaleReceiptEditingPayload {
oldSaleReceipt: SaleReceipt; oldSaleReceipt: SaleReceipt;
saleReceiptDTO: ISaleReceiptDTO; saleReceiptDTO: EditSaleReceiptDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ISaleReceiptEventClosedPayload { export interface ISaleReceiptEventClosedPayload {

View File

@@ -15,7 +15,7 @@ export class AttachmentUploadPipeline {
* @param res The HTTP response object. * @param res The HTTP response object.
* @param next The callback to pass control to the next middleware function. * @param next The callback to pass control to the next middleware function.
*/ */
public validateS3Configured(req: Request, res: Response, next: NextFunction) { validateS3Configured(req: Request, res: Response, next: NextFunction) {
if ( if (
!config.s3.region || !config.s3.region ||
!config.s3.accessKeyId || !config.s3.accessKeyId ||
@@ -42,6 +42,7 @@ export class AttachmentUploadPipeline {
s3, s3,
bucket: config.s3.bucket, bucket: config.s3.bucket,
contentType: multerS3.AUTO_CONTENT_TYPE, contentType: multerS3.AUTO_CONTENT_TYPE,
metadata: function (req, file, cb) { metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname }); cb(null, { fieldName: file.fieldname });
}, },

15
pnpm-lock.yaml generated
View File

@@ -547,6 +547,9 @@ importers:
'@supercharge/promise-pool': '@supercharge/promise-pool':
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0 version: 3.2.0
'@types/multer':
specifier: ^1.4.11
version: 1.4.11
'@types/nodemailer': '@types/nodemailer':
specifier: ^6.4.17 specifier: ^6.4.17
version: 6.4.17 version: 6.4.17
@@ -631,6 +634,9 @@ importers:
mathjs: mathjs:
specifier: ^9.4.0 specifier: ^9.4.0
version: 9.5.2 version: 9.5.2
mime-types:
specifier: ^2.1.35
version: 2.1.35
moment: moment:
specifier: ^2.30.1 specifier: ^2.30.1
version: 2.30.1 version: 2.30.1
@@ -640,6 +646,12 @@ importers:
moment-timezone: moment-timezone:
specifier: ^0.5.43 specifier: ^0.5.43
version: 0.5.45 version: 0.5.45
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
multer-s3:
specifier: ^3.0.1
version: 3.0.1(@aws-sdk/client-s3@3.583.0)
mysql: mysql:
specifier: ^2.18.1 specifier: ^2.18.1
version: 2.18.1 version: 2.18.1
@@ -12460,7 +12472,6 @@ packages:
resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==} resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==}
dependencies: dependencies:
'@types/express': 4.17.21 '@types/express': 4.17.21
dev: true
/@types/node-fetch@2.6.11: /@types/node-fetch@2.6.11:
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
@@ -12653,7 +12664,7 @@ packages:
/@types/serve-index@1.9.4: /@types/serve-index@1.9.4:
resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==}
dependencies: dependencies:
'@types/express': 4.17.21 '@types/express': 5.0.0
dev: false dev: false
/@types/serve-static@1.15.7: /@types/serve-static@1.15.7: