fix: invoice generate sharable link

This commit is contained in:
Ahmed Bouhuolia
2025-06-27 01:59:46 +02:00
parent e7178a6575
commit 0c0e1dc22e
10 changed files with 74 additions and 19 deletions

View File

@@ -1,9 +1,9 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service'; import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service';
import { ItemCategory } from '../models/ItemCategory.model'; import { ItemCategory } from '../models/ItemCategory.model';
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { IItemCategoryDeletedPayload } from '../ItemCategory.interfaces'; import { IItemCategoryDeletedPayload } from '../ItemCategory.interfaces';
import { Item } from '@/modules/Items/models/Item'; import { Item } from '@/modules/Items/models/Item';
@@ -46,7 +46,10 @@ export class DeleteItemCategoryService {
await this.unassociateItemsWithCategories(itemCategoryId, trx); await this.unassociateItemsWithCategories(itemCategoryId, trx);
// Delete item category. // Delete item category.
await ItemCategory.query(trx).findById(itemCategoryId).delete(); await this.itemCategoryModel()
.query(trx)
.findById(itemCategoryId)
.delete();
// Triggers `onItemCategoryDeleted` event. // Triggers `onItemCategoryDeleted` event.
await this.eventEmitter.emitAsync(events.itemCategory.onDeleted, { await this.eventEmitter.emitAsync(events.itemCategory.onDeleted, {

View File

@@ -1,4 +1,5 @@
import * as moment from 'moment'; import * as moment from 'moment';
import { ClsService } from 'nestjs-cls';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel'; import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice'; import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice';
@@ -6,8 +7,6 @@ import { TransformerInjectable } from '../Transformer/TransformerInjectable.serv
import { PaymentLink } from './models/PaymentLink'; import { PaymentLink } from './models/PaymentLink';
import { ServiceError } from '../Items/ServiceError'; import { ServiceError } from '../Items/ServiceError';
import { GetInvoicePaymentLinkMetaTransformer } from '../SaleInvoices/queries/GetInvoicePaymentLink.transformer'; import { GetInvoicePaymentLinkMetaTransformer } from '../SaleInvoices/queries/GetInvoicePaymentLink.transformer';
import { ClsService } from 'nestjs-cls';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenantModel } from '../System/models/TenantModel'; import { TenantModel } from '../System/models/TenantModel';
@Injectable() @Injectable()
@@ -15,7 +14,6 @@ export class GetInvoicePaymentLinkMetadata {
constructor( constructor(
private readonly transformer: TransformerInjectable, private readonly transformer: TransformerInjectable,
private readonly clsService: ClsService, private readonly clsService: ClsService,
private readonly tenancyContext: TenancyContext,
@Inject(SaleInvoice.name) @Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>, private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,

View File

@@ -25,10 +25,7 @@ export class PaymentLinksController {
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
data: { data: { type: 'object', description: 'Payment link metadata' },
type: 'object',
description: 'Payment link metadata',
},
}, },
}, },
}) })

View File

@@ -22,6 +22,7 @@ import {
CreateSaleInvoiceDto, CreateSaleInvoiceDto,
EditSaleInvoiceDto, EditSaleInvoiceDto,
} from './dtos/SaleInvoice.dto'; } from './dtos/SaleInvoice.dto';
import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service';
@Injectable() @Injectable()
export class SaleInvoiceApplication { export class SaleInvoiceApplication {
@@ -39,6 +40,7 @@ export class SaleInvoiceApplication {
private getSaleInvoiceStateService: GetSaleInvoiceState, private getSaleInvoiceStateService: GetSaleInvoiceState,
private sendSaleInvoiceMailService: SendSaleInvoiceMail, private sendSaleInvoiceMailService: SendSaleInvoiceMail,
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState, private getSaleInvoiceMailStateService: GetSaleInvoiceMailState,
private generateShareLinkService: GenerateShareLink,
) {} ) {}
/** /**
@@ -202,4 +204,23 @@ export class SaleInvoiceApplication {
saleInvoiceid, saleInvoiceid,
); );
} }
/**
* Generate the given sale invoice sharable link.
* @param {number} saleInvoiceId
* @param {string} publicity
* @param {string} expiryTime
* @returns
*/
public generateSaleInvoiceSharableLink(
saleInvoiceId: number,
publicity: string = 'private',
expiryTime: string = '',
) {
return this.generateShareLinkService.generatePaymentLink(
saleInvoiceId,
publicity,
expiryTime,
);
}
} }

View File

@@ -38,12 +38,14 @@ import { AcceptType } from '@/constants/accept-type';
import { SaleInvoiceResponseDto } from './dtos/SaleInvoiceResponse.dto'; import { SaleInvoiceResponseDto } from './dtos/SaleInvoiceResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto'; import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
import { SaleInvoiceStateResponseDto } from './dtos/SaleInvoiceState.dto'; import { SaleInvoiceStateResponseDto } from './dtos/SaleInvoiceState.dto';
import { GenerateSaleInvoiceSharableLinkResponseDto } from './dtos/generateSaleInvoiceSharableLinkResponse.dto';
@Controller('sale-invoices') @Controller('sale-invoices')
@ApiTags('Sale Invoices') @ApiTags('Sale Invoices')
@ApiExtraModels(SaleInvoiceResponseDto) @ApiExtraModels(SaleInvoiceResponseDto)
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiExtraModels(SaleInvoiceStateResponseDto) @ApiExtraModels(SaleInvoiceStateResponseDto)
@ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto)
@ApiHeader({ @ApiHeader({
name: 'organization-id', name: 'organization-id',
description: 'The organization id', description: 'The organization id',
@@ -318,4 +320,25 @@ export class SaleInvoicesController {
): Promise<SaleInvoiceMailState> { ): Promise<SaleInvoiceMailState> {
return this.saleInvoiceApplication.getSaleInvoiceMailState(id); return this.saleInvoiceApplication.getSaleInvoiceMailState(id);
} }
@Post(':id/generate-link')
@ApiOperation({
summary: 'Generate sharable sale invoice link (private or public)',
})
@ApiResponse({
status: 201,
description: 'The link has been generated successfully.',
schema: {
$ref: getSchemaPath(GenerateSaleInvoiceSharableLinkResponseDto),
},
})
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The sale invoice id',
})
generateSaleInvoiceSharableLink(@Param('id', ParseIntPipe) id: number) {
return this.saleInvoiceApplication.generateSaleInvoiceSharableLink(id);
}
} }

View File

@@ -9,6 +9,7 @@ import { events } from '@/common/events/events';
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink'; import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleInvoice } from '../models/SaleInvoice'; import { SaleInvoice } from '../models/SaleInvoice';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class GenerateShareLink { export class GenerateShareLink {
@@ -16,12 +17,13 @@ export class GenerateShareLink {
private uow: UnitOfWork, private uow: UnitOfWork,
private eventPublisher: EventEmitter2, private eventPublisher: EventEmitter2,
private transformer: TransformerInjectable, private transformer: TransformerInjectable,
private tenancyContext: TenancyContext,
@Inject(SaleInvoice.name) @Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>, private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PaymentLink.name) @Inject(PaymentLink.name)
private paymentLinkModel: TenantModelProxy<typeof PaymentLink>, private paymentLinkModel: typeof PaymentLink,
) {} ) {}
/** /**
@@ -39,10 +41,10 @@ export class GenerateShareLink {
.query() .query()
.findById(saleInvoiceId) .findById(saleInvoiceId)
.throwIfNotFound(); .throwIfNotFound();
const tenant = await this.tenancyContext.getTenant();
// Generate unique uuid for sharable link. // Generate unique uuid for sharable link.
const linkId = uuidv4() as string; const linkId = uuidv4() as string;
const commonEventPayload = { const commonEventPayload = {
saleInvoiceId, saleInvoiceId,
publicity, publicity,
@@ -54,11 +56,12 @@ export class GenerateShareLink {
events.saleInvoice.onPublicLinkGenerating, events.saleInvoice.onPublicLinkGenerating,
{ ...commonEventPayload, trx }, { ...commonEventPayload, trx },
); );
const paymentLink = await this.paymentLinkModel().query().insert({ const paymentLink = await this.paymentLinkModel.query().insert({
linkId, linkId,
publicity, publicity,
resourceId: foundInvoice.id, resourceId: foundInvoice.id,
resourceType: 'SaleInvoice', resourceType: 'SaleInvoice',
tenantId: tenant.id,
}); });
// Triggers `onPublicSharableLinkGenerated` event. // Triggers `onPublicSharableLinkGenerated` event.
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class GenerateSaleInvoiceSharableLinkResponseDto {
@ApiProperty({
description: 'Sharable payment link for the sale invoice',
example:
'http://localhost:3000/payment/123e4567-e89b-12d3-a456-426614174000',
})
link: string;
}

View File

@@ -29,7 +29,6 @@ import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry'; import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry';
import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
import { CreditNote } from '@/modules/CreditNotes/models/CreditNote'; import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt'; import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt';
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal'; import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry'; import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry';
@@ -70,7 +69,6 @@ const models = [
CreditNoteAppliedInvoice, CreditNoteAppliedInvoice,
CreditNote, CreditNote,
RefundCreditNote, RefundCreditNote,
PaymentLink,
SaleReceipt, SaleReceipt,
ManualJournal, ManualJournal,
ManualJournalEntry, ManualJournalEntry,
@@ -82,7 +80,6 @@ const models = [
TenantUser, TenantUser,
]; ];
/** /**
* Decorator factory that registers a model with the tenancy system. * Decorator factory that registers a model with the tenancy system.
* @param model The model class to register * @param model The model class to register

View File

@@ -49,7 +49,7 @@ export const SharePaymentLinkForm = ({
generateShareLink(values) generateShareLink(values)
.then((res) => { .then((res) => {
setSubmitting(false); setSubmitting(false);
setUrl(res.link?.link); setUrl(res.link);
}) })
.catch(() => { .catch(() => {
setSubmitting(false); setSubmitting(false);

View File

@@ -45,7 +45,10 @@ export function useCreatePaymentLink(
return useMutation<CreatePaymentLinkResponse, Error, CreatePaymentLinkValues>( return useMutation<CreatePaymentLinkResponse, Error, CreatePaymentLinkValues>(
(values) => (values) =>
apiRequest apiRequest
.post('/payment-links/generate', transfromToSnakeCase(values)) .post(
`/sale-invoices/${values.transactionId}/generate-link`,
transfromToSnakeCase(values),
)
.then((res) => res.data), .then((res) => res.data),
{ {
...options, ...options,