diff --git a/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts b/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts index fe68f14a1..ea31ec2f0 100644 --- a/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts +++ b/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts @@ -22,6 +22,13 @@ export class PublicSharableLinkController extends BaseController { this.getPaymentLinkPublicMeta.bind(this), this.validationResult ); + router.get( + '/:paymentLinkId/invoice/pdf', + [param('paymentLinkId').exists()], + this.validationResult, + this.getPaymentLinkInvoicePdf.bind(this), + this.validationResult + ); router.post( '/:paymentLinkId/stripe_checkout_session', [param('paymentLinkId').exists()], @@ -80,4 +87,31 @@ export class PublicSharableLinkController extends BaseController { next(error); } } + + /** + * Retrieves the sale invoice pdf of the given payment link. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getPaymentLinkInvoicePdf( + req: Request<{ paymentLinkId: string }>, + res: Response, + next: NextFunction + ) { + const { paymentLinkId } = req.params; + + try { + const pdfContent = await this.paymentLinkApp.getPaymentLinkInvoicePdf( + paymentLinkId + ); + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': pdfContent.length, + }); + res.send(pdfContent); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/services/PaymentLinks/GetPaymentLinkInvoicePdf.ts b/packages/server/src/services/PaymentLinks/GetPaymentLinkInvoicePdf.ts new file mode 100644 index 000000000..7122cfea2 --- /dev/null +++ b/packages/server/src/services/PaymentLinks/GetPaymentLinkInvoicePdf.ts @@ -0,0 +1,33 @@ +import { Inject, Service } from 'typedi'; +import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection'; +import { SaleInvoicePdf } from '../Sales/Invoices/SaleInvoicePdf'; +import { PaymentLink } from '@/system/models'; + +@Service() +export class GetPaymentLinkInvoicePdf { + @Inject() + private getSaleInvoicePdfService: SaleInvoicePdf; + + /** + * Retrieves the sale invoice PDF of the given payment link id. + * @param {number} tenantId + * @param {number} paymentLinkId + * @returns {Promise} + */ + async getPaymentLinkInvoicePdf(paymentLinkId: string): Promise { + const paymentLink = await PaymentLink.query() + .findOne('linkId', paymentLinkId) + .where('resourceType', 'SaleInvoice') + .throwIfNotFound(); + + const tenantId = paymentLink.tenantId; + await initalizeTenantServices(tenantId); + + const saleInvoiceId = paymentLink.resourceId; + + return this.getSaleInvoicePdfService.saleInvoicePdf( + tenantId, + saleInvoiceId + ); + } +} diff --git a/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts b/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts index 641521558..2ba46dd8a 100644 --- a/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts +++ b/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts @@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi'; import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata'; import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; +import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf'; @Service() export class PaymentLinksApplication { @@ -10,6 +11,9 @@ export class PaymentLinksApplication { @Inject() private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; + + @Inject() + private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf; /** * Retrieves the invoice payment link. @@ -34,4 +38,16 @@ export class PaymentLinksApplication { paymentLinkId ); } + + /** + * Retrieves the sale invoice pdf of the given payment link id. + * @param {number} tenantId + * @param {number} paymentLinkId + * @returns + */ + public getPaymentLinkInvoicePdf(paymentLinkId: string) { + return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf( + paymentLinkId + ); + } } diff --git a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx index 6e1080910..94b633af0 100644 --- a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx +++ b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx @@ -1,10 +1,14 @@ -import { Text, Classes, Button, Intent, Tag } from '@blueprintjs/core'; +import { Text, Classes, Button, Intent } from '@blueprintjs/core'; import clsx from 'classnames'; import { AppToaster, Box, Group, Stack } from '@/components'; import { usePaymentPortalBoot } from './PaymentPortalBoot'; import { useDrawerActions } from '@/hooks/state'; -import { useCreateStripeCheckoutSession } from '@/hooks/query/payment-link'; +import { + useCreateStripeCheckoutSession, + useGeneratePaymentLinkInvoicePdf, +} from '@/hooks/query/payment-link'; import { DRAWERS } from '@/constants/drawers'; +import { downloadFile } from '@/hooks/useDownloadFile'; import styles from './PaymentPortal.module.scss'; export function PaymentPortal() { @@ -15,10 +19,30 @@ export function PaymentPortal() { isLoading: isStripeCheckoutLoading, } = useCreateStripeCheckoutSession(); + const { + mutateAsync: generatePaymentLinkInvoice, + isLoading: isInvoiceGenerating, + } = useGeneratePaymentLinkInvoicePdf(); + // Handles invoice preview button click. const handleInvoicePreviewBtnClick = () => { openDrawer(DRAWERS.PAYMENT_INVOICE_PREVIEW); }; + + // Handles invoice download button click. + const handleInvoiceDownloadBtnClick = () => { + generatePaymentLinkInvoice({ paymentLinkId: linkId }) + .then((data) => { + downloadFile(data, `Invoice ${sharableLinkMeta?.invoiceNo}.pdf`); + }) + .catch(() => { + AppToaster.show({ + intent: Intent.DANGER, + message: 'Something went wrong.', + }); + }); + }; + // handles the pay button click. const handlePayButtonClick = () => { createStripeCheckoutSession({ linkId }) @@ -125,6 +149,8 @@ export function PaymentPortal() { diff --git a/packages/webapp/src/hooks/query/payment-link.ts b/packages/webapp/src/hooks/query/payment-link.ts index 96cce6c0f..f1e10f0fc 100644 --- a/packages/webapp/src/hooks/query/payment-link.ts +++ b/packages/webapp/src/hooks/query/payment-link.ts @@ -11,6 +11,7 @@ import useApiRequest from '../useRequest'; import { transformToCamelCase, transfromToSnakeCase } from '@/utils'; const GetPaymentLinkInvoice = 'GetPaymentLinkInvoice'; +const GetPaymentLinkInvoicePdf = 'GetPaymentLinkInvoicePdf'; // Create Payment Link // ------------------------------------ @@ -170,3 +171,56 @@ export const useCreateStripeCheckoutSession = ( { ...options }, ); }; + +// Get Payment Link Invoice PDF +// ------------------------------------ +interface GetPaymentLinkInvoicePdfResponse {} + +interface GeneratePaymentLinkInvoicePdfValues { + paymentLinkId: string; +} + +export const useGeneratePaymentLinkInvoicePdf = ( + options?: UseMutationOptions< + GetPaymentLinkInvoicePdfResponse, + Error, + GeneratePaymentLinkInvoicePdfValues + >, +): UseMutationResult< + GetPaymentLinkInvoicePdfResponse, + Error, + GeneratePaymentLinkInvoicePdfValues +> => { + const apiRequest = useApiRequest(); + + return useMutation< + GetPaymentLinkInvoicePdfResponse, + Error, + GeneratePaymentLinkInvoicePdfValues + >( + (values: GeneratePaymentLinkInvoicePdfValues) => { + return apiRequest + .get(`/payment-links/${values.paymentLinkId}/invoice/pdf`) + .then((res) => res?.data); + }, + { ...options }, + ); +}; + +export const useGetPaymentLinkInvoicePdf = ( + invoiceId: string, + options?: UseQueryOptions, +): UseQueryResult => { + const apiRequest = useApiRequest(); + + return useQuery( + [GetPaymentLinkInvoicePdf, invoiceId], + () => + apiRequest + .get(`/payment-links/${invoiceId}/invoice/pdf`) + .then((res) => res.data), + { + ...options, + }, + ); +}; diff --git a/packages/webapp/src/hooks/useDownloadFile.ts b/packages/webapp/src/hooks/useDownloadFile.ts index 3237f2a79..669558838 100644 --- a/packages/webapp/src/hooks/useDownloadFile.ts +++ b/packages/webapp/src/hooks/useDownloadFile.ts @@ -33,9 +33,15 @@ export const useDownloadFile = (args: IArgs) => { return { ...mutation }; }; -export function downloadFile(data, filename, mime, bom) { +export function downloadFile( + data, + filename, + mime = 'application/octet-stream', + bom?: any, +) { var blobData = typeof bom !== 'undefined' ? [bom, data] : [data]; - var blob = new Blob(blobData, { type: mime || 'application/octet-stream' }); + var blob = new Blob(blobData, { type: mime }); + if (typeof window.navigator.msSaveBlob !== 'undefined') { // IE workaround for "HTML7007: One or more blob URLs were // revoked by closing the blob for which they were created.