tango-app-api-payment-subscription 3.4.3 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/invoice-approval-pipeline.md +44 -0
- package/package.json +8 -3
- package/scripts/grant-tango-approval-permissions.js +84 -0
- package/scripts/migrate-invoice-status-pipeline.js +61 -0
- package/src/controllers/applicationDefault.controllers.js +51 -0
- package/src/controllers/billing.controllers.js +2 -1
- package/src/controllers/brandsBilling.controller.js +387 -6
- package/src/controllers/invoice.controller.js +524 -30
- package/src/controllers/payment.controller.js +4 -3
- package/src/controllers/paymentSubscription.controllers.js +37 -14
- package/src/dtos/validation.dtos.js +55 -0
- package/src/hbs/invoicePdf.hbs +8 -0
- package/src/routes/brandsBilling.routes.js +17 -1
- package/src/routes/invoice.routes.js +12 -2
- package/src/services/applicationDefault.service.js +13 -0
- package/src/utils/currency.js +14 -0
|
@@ -9,8 +9,58 @@ import Handlebars from '../utils/validations/helper/handlebar.helper.js';
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import htmlpdf from 'html-pdf-node';
|
|
12
|
+
import archiver from 'archiver';
|
|
12
13
|
import * as basepricingService from '../services/basePrice.service.js';
|
|
13
14
|
import * as paymentAccountService from '../services/paymentAccount.service.js';
|
|
15
|
+
import { symbolFor } from '../utils/currency.js';
|
|
16
|
+
import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
|
|
17
|
+
import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
|
|
18
|
+
import * as assignedStoreService from '../services/assignedStore.service.js';
|
|
19
|
+
|
|
20
|
+
// Pulls CSM + Finance head emails (stored under applicationDefault
|
|
21
|
+
// type=invoice, subType=heads) AND the per-client CSMs assigned via
|
|
22
|
+
// userAssignedStore (clientId + tangoUserType:'csm'). Returns a flat de-duped
|
|
23
|
+
// array suitable for the SES CC field. Returns [] on any failure — invoice
|
|
24
|
+
// email send should never fail because of missing settings or DB hiccups.
|
|
25
|
+
async function getInvoiceCcEmails( clientId ) {
|
|
26
|
+
const collected = [];
|
|
27
|
+
|
|
28
|
+
// 1. Configured heads (global, applies to every invoice).
|
|
29
|
+
try {
|
|
30
|
+
const doc = await findOneApplicationDefault( { type: 'invoice', subType: 'heads' } );
|
|
31
|
+
const rows = ( doc && Array.isArray( doc.data ) ) ? doc.data : [];
|
|
32
|
+
for ( const row of rows ) {
|
|
33
|
+
const value = row?.email;
|
|
34
|
+
if ( Array.isArray( value ) ) {
|
|
35
|
+
for ( const e of value ) {
|
|
36
|
+
if ( typeof e === 'string' && e.trim() !== '' ) collected.push( e.trim() );
|
|
37
|
+
}
|
|
38
|
+
} else if ( typeof value === 'string' && value.trim() !== '' ) {
|
|
39
|
+
collected.push( value.trim() );
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch ( err ) {
|
|
43
|
+
logger.error( { error: err, function: 'getInvoiceCcEmails.heads' } );
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Per-client CSMs from userAssignedStore.
|
|
47
|
+
if ( clientId ) {
|
|
48
|
+
try {
|
|
49
|
+
const assigned = await assignedStoreService.find(
|
|
50
|
+
{ clientId, tangoUserType: 'csm' },
|
|
51
|
+
{ userEmail: 1 },
|
|
52
|
+
);
|
|
53
|
+
for ( const a of ( assigned || [] ) ) {
|
|
54
|
+
const e = a?.userEmail;
|
|
55
|
+
if ( typeof e === 'string' && e.trim() !== '' ) collected.push( e.trim() );
|
|
56
|
+
}
|
|
57
|
+
} catch ( err ) {
|
|
58
|
+
logger.error( { error: err, function: 'getInvoiceCcEmails.csm', clientId } );
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.from( new Set( collected ) );
|
|
63
|
+
}
|
|
14
64
|
|
|
15
65
|
export async function createInvoice( req, res ) {
|
|
16
66
|
try {
|
|
@@ -129,7 +179,7 @@ export async function createInvoice( req, res ) {
|
|
|
129
179
|
groupId: group._id,
|
|
130
180
|
invoice: req.body.invoiceId ? req.body.invoiceId : `INV-${Finacialyear}-${invoiceNo}`,
|
|
131
181
|
products: products,
|
|
132
|
-
status: '
|
|
182
|
+
status: 'pendingCsm',
|
|
133
183
|
amount: Math.round( amount ),
|
|
134
184
|
invoiceIndex: req.body.invoiceId ? findInvoice.invoiceIndex : invoiceNo,
|
|
135
185
|
tax: taxList,
|
|
@@ -220,9 +270,9 @@ export async function invoiceDownload( req, res ) {
|
|
|
220
270
|
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
221
271
|
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
222
272
|
item.productName = firstWord + ' ' + secondWord;
|
|
223
|
-
item.price = item.price.toLocaleString( 'en-IN'
|
|
273
|
+
item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
|
|
224
274
|
item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
225
|
-
item.currency = clientDetails?.paymentInvoice?.currencyType
|
|
275
|
+
item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
|
|
226
276
|
} );
|
|
227
277
|
|
|
228
278
|
|
|
@@ -254,7 +304,7 @@ export async function invoiceDownload( req, res ) {
|
|
|
254
304
|
PoNum: getgroup?.po,
|
|
255
305
|
amountwords: AmountinWords,
|
|
256
306
|
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
257
|
-
currencyType: virtualAccount?.currency
|
|
307
|
+
currencyType: symbolFor( virtualAccount?.currency ),
|
|
258
308
|
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
|
|
259
309
|
invoiceDate,
|
|
260
310
|
dueDate,
|
|
@@ -325,7 +375,7 @@ export async function invoiceDownload( req, res ) {
|
|
|
325
375
|
storeName: '$stores.storeName',
|
|
326
376
|
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
327
377
|
workingdays: '$stores.products.workingdays',
|
|
328
|
-
currencyType: { $literal: clientDetails?.paymentInvoice?.currencyType
|
|
378
|
+
currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
|
|
329
379
|
},
|
|
330
380
|
},
|
|
331
381
|
{
|
|
@@ -430,7 +480,16 @@ export async function invoiceDownload( req, res ) {
|
|
|
430
480
|
content: html,
|
|
431
481
|
};
|
|
432
482
|
let options = {
|
|
433
|
-
|
|
483
|
+
executablePath: '/usr/bin/chromium',
|
|
484
|
+
args: [
|
|
485
|
+
'--no-sandbox',
|
|
486
|
+
'--disable-setuid-sandbox',
|
|
487
|
+
'--disable-dev-shm-usage',
|
|
488
|
+
'--no-zygote',
|
|
489
|
+
'--single-process',
|
|
490
|
+
],
|
|
491
|
+
format: 'A4',
|
|
492
|
+
margin: {
|
|
434
493
|
top: '0.5in',
|
|
435
494
|
right: '0.5in',
|
|
436
495
|
bottom: '0.5in',
|
|
@@ -459,9 +518,12 @@ export async function invoiceDownload( req, res ) {
|
|
|
459
518
|
}
|
|
460
519
|
const SES = JSON.parse( process.env.SES );
|
|
461
520
|
let fromEmail = SES.accountsEmail;
|
|
462
|
-
|
|
521
|
+
// Load configured CSM + Finance heads PLUS the per-client CSMs from
|
|
522
|
+
// userAssignedStore as CC recipients on the invoice mail.
|
|
523
|
+
const ccEmails = await getInvoiceCcEmails( invoiceInfo.clientId );
|
|
524
|
+
console.log( fromEmail, getgroup.generateInvoiceTo, ccEmails, attachments );
|
|
463
525
|
|
|
464
|
-
const result = await sendEmailWithSES( getgroup.generateInvoiceTo, mailSubject, mailbody, attachments, fromEmail );
|
|
526
|
+
const result = await sendEmailWithSES( getgroup.generateInvoiceTo, mailSubject, mailbody, attachments, fromEmail, ccEmails.length ? ccEmails : undefined );
|
|
465
527
|
console.log( result );
|
|
466
528
|
let logObj = {
|
|
467
529
|
userName: req.user?.userName,
|
|
@@ -478,11 +540,28 @@ export async function invoiceDownload( req, res ) {
|
|
|
478
540
|
console.log( logObj );
|
|
479
541
|
insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
|
|
480
542
|
if ( result ) {
|
|
543
|
+
// Pipeline guard: this approve-via-download flow can only complete
|
|
544
|
+
// the final stage. Earlier stages must use /approveInvoiceCsm
|
|
545
|
+
// and /approveInvoiceFinance.
|
|
546
|
+
const current = await invoiceService.findOne( { _id: req.params.invoiceId } );
|
|
547
|
+
if ( current?.status !== 'pendingApproval' ) {
|
|
548
|
+
return res.sendError(
|
|
549
|
+
`Cannot approve via download — invoice is at status '${current?.status}'. Advance it through the approval stages first.`,
|
|
550
|
+
409,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
481
553
|
await invoiceService.updateOne( { _id: req.params.invoiceId }, { status: req.body.status } );
|
|
482
554
|
return res.sendSuccess( result );
|
|
483
555
|
}
|
|
484
556
|
}
|
|
485
|
-
|
|
557
|
+
const invoiceMonth = invoiceInfo.billingDate ?
|
|
558
|
+
dayjs( invoiceInfo.billingDate ).format( 'MMMM' ) :
|
|
559
|
+
( invoiceInfo.monthOfbilling ? dayjs( invoiceInfo.monthOfbilling, 'MM' ).format( 'MMMM' ) : '' );
|
|
560
|
+
const brandName = getgroup?.registeredCompanyName || invoiceInfo.companyName || '';
|
|
561
|
+
const safeFilename = `${invoiceInfo.invoice}-${invoiceMonth}-${brandName}.pdf`
|
|
562
|
+
.replace( /[\r\n"\\]/g, '' )
|
|
563
|
+
.trim();
|
|
564
|
+
res.set( 'Content-Disposition', `attachment; filename="${safeFilename}"` );
|
|
486
565
|
res.set( 'Content-Type', 'application/pdf' );
|
|
487
566
|
res.send( pdfBuffer );
|
|
488
567
|
} );
|
|
@@ -493,6 +572,316 @@ export async function invoiceDownload( req, res ) {
|
|
|
493
572
|
return res.sendError( error, 500 );
|
|
494
573
|
}
|
|
495
574
|
}
|
|
575
|
+
|
|
576
|
+
async function buildInvoicePdfBuffer( invoiceId ) {
|
|
577
|
+
let invoiceInfo = await invoiceService.findOne( { _id: invoiceId } );
|
|
578
|
+
if ( !invoiceInfo ) {
|
|
579
|
+
throw new Error( `Invoice ${invoiceId} not found` );
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
|
|
583
|
+
invoiceInfo.products.forEach( ( item, index ) => {
|
|
584
|
+
item.index = index + 1;
|
|
585
|
+
let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
|
|
586
|
+
firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
|
|
587
|
+
item.productName = firstWord + ' ' + secondWord;
|
|
588
|
+
item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
|
|
589
|
+
item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
590
|
+
item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
|
|
591
|
+
} );
|
|
592
|
+
|
|
593
|
+
let invoiceDate = dayjs( invoiceInfo.billingDate ).format( 'DD/MM/YYYY' );
|
|
594
|
+
invoiceInfo.totalAmount = Math.round( invoiceInfo.totalAmount );
|
|
595
|
+
let AmountinWords = inWords( invoiceInfo.totalAmount );
|
|
596
|
+
let getgroup;
|
|
597
|
+
if ( invoiceInfo.groupId ) {
|
|
598
|
+
getgroup = await billingService.findOne( { _id: invoiceInfo.groupId } );
|
|
599
|
+
}
|
|
600
|
+
let days = getgroup?.paymentTerm ? getgroup?.paymentTerm : '30';
|
|
601
|
+
let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
|
|
602
|
+
|
|
603
|
+
let virtualAccount = await paymentAccountService.findOneAccount( { clientId: invoiceInfo.clientId } );
|
|
604
|
+
|
|
605
|
+
let invoiceData = {
|
|
606
|
+
...invoiceInfo._doc,
|
|
607
|
+
clientName: clientDetails.clientName,
|
|
608
|
+
amount: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
|
|
609
|
+
extendDays: getgroup?.paymentTerm ? getgroup?.paymentTerm : '30',
|
|
610
|
+
address: clientDetails.billingDetails.billingAddress,
|
|
611
|
+
subtotal: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
|
|
612
|
+
companyName: invoiceInfo.companyName,
|
|
613
|
+
companyAddress: invoiceInfo.companyAddress,
|
|
614
|
+
PlaceOfSupply: invoiceInfo.PlaceOfSupply,
|
|
615
|
+
GSTNumber: invoiceInfo.GSTNumber,
|
|
616
|
+
PoNum: getgroup?.po,
|
|
617
|
+
amountwords: AmountinWords,
|
|
618
|
+
Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
|
|
619
|
+
currencyType: symbolFor( virtualAccount?.currency ),
|
|
620
|
+
totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
|
|
621
|
+
invoiceDate,
|
|
622
|
+
dueDate,
|
|
623
|
+
discountPercentage: invoiceInfo.discountPercentage ? invoiceInfo.discountPercentage : 0,
|
|
624
|
+
discountAmount: invoiceInfo.discountAmount ? invoiceInfo.discountAmount : 0,
|
|
625
|
+
logo: `${JSON.parse( process.env.URL ).apiDomain}/logo.png`,
|
|
626
|
+
uidomain: `${JSON.parse( process.env.URL ).domain}`,
|
|
627
|
+
attachAnnexure: getgroup?.attachAnnexure,
|
|
628
|
+
billingAddressLineOne: getgroup?.addressLineOne ? getgroup?.addressLineOne : invoiceInfo?.companyAddress,
|
|
629
|
+
billingAddressLineTwo: getgroup?.addressLineTwo,
|
|
630
|
+
billingCountry: getgroup?.country,
|
|
631
|
+
billingState: getgroup?.state,
|
|
632
|
+
billingCity: getgroup?.city,
|
|
633
|
+
billingPinCode: getgroup?.pinCode,
|
|
634
|
+
billingCurrency: virtualAccount?.currency,
|
|
635
|
+
virtualaccountNumber: virtualAccount ? virtualAccount?.accountNumber : '',
|
|
636
|
+
virtualifsc: virtualAccount ? virtualAccount?.ifsc : '',
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
if ( invoiceData?.tax?.length ) {
|
|
640
|
+
invoiceData.tax.forEach( ( item ) => {
|
|
641
|
+
if ( item.taxAmount ) {
|
|
642
|
+
item.taxAmount = Number( item.taxAmount );
|
|
643
|
+
item.taxAmount = item.taxAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
644
|
+
}
|
|
645
|
+
} );
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const currentMonthDays = dayjs().daysInMonth();
|
|
649
|
+
|
|
650
|
+
if ( getgroup?.attachAnnexure ) {
|
|
651
|
+
let annuxureData = await dailyPricingService.aggregate( [
|
|
652
|
+
{ $match: { clientId: invoiceInfo.clientId } },
|
|
653
|
+
{ $sort: { dateISO: -1 } },
|
|
654
|
+
{ $limit: 1 },
|
|
655
|
+
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ] } } } } },
|
|
656
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
657
|
+
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
658
|
+
{ $project: {
|
|
659
|
+
productName: '$stores.products.productName',
|
|
660
|
+
storeId: '$stores.storeId',
|
|
661
|
+
storeName: '$stores.storeName',
|
|
662
|
+
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
663
|
+
workingdays: '$stores.products.workingdays',
|
|
664
|
+
currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
|
|
665
|
+
} },
|
|
666
|
+
{ $sort: { productName: 1, workingdays: -1 } },
|
|
667
|
+
{ $match: { workingdays: { $gt: 0 } } },
|
|
668
|
+
{ $lookup: {
|
|
669
|
+
from: 'basepricings',
|
|
670
|
+
let: { clientId: invoiceInfo.clientId },
|
|
671
|
+
pipeline: [
|
|
672
|
+
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
673
|
+
{ $project: { standard: 1, step: 1 } },
|
|
674
|
+
],
|
|
675
|
+
as: 'basepricing',
|
|
676
|
+
} },
|
|
677
|
+
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
678
|
+
{ $project: {
|
|
679
|
+
productName: 1,
|
|
680
|
+
workingdays: 1,
|
|
681
|
+
storeName: 1,
|
|
682
|
+
currencyType: 1,
|
|
683
|
+
edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
|
|
684
|
+
storeId: 1,
|
|
685
|
+
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
686
|
+
step: '$basepricing.step',
|
|
687
|
+
} },
|
|
688
|
+
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
689
|
+
{ $project: {
|
|
690
|
+
productName: {
|
|
691
|
+
$concat: [
|
|
692
|
+
{ $toUpper: { $substr: [ '$productName', 0, 1 ] } },
|
|
693
|
+
{ $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] },
|
|
694
|
+
],
|
|
695
|
+
},
|
|
696
|
+
currencyType: 1,
|
|
697
|
+
workingdays: 1,
|
|
698
|
+
storeName: 1,
|
|
699
|
+
edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
|
|
700
|
+
storeId: 1,
|
|
701
|
+
period: { $cond: { if: { $lt: [ '$workingdays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
|
|
702
|
+
standardPrice: '$standard.negotiatePrice',
|
|
703
|
+
runningCost: { $round: [ { $multiply: [ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] }, '$workingdays' ] }, 2 ] },
|
|
704
|
+
} },
|
|
705
|
+
] );
|
|
706
|
+
invoiceData.annuxureData = annuxureData;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
|
|
710
|
+
const template = Handlebars.compile( templateHtml );
|
|
711
|
+
const html = template( { ...invoiceData } );
|
|
712
|
+
let file = { content: html };
|
|
713
|
+
let options = {
|
|
714
|
+
executablePath: '/usr/bin/chromium',
|
|
715
|
+
args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--no-zygote', '--single-process' ],
|
|
716
|
+
format: 'A4',
|
|
717
|
+
margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' },
|
|
718
|
+
printBackground: true,
|
|
719
|
+
preferCSSPageSize: true,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const pdfBuffer = await htmlpdf.generatePdf( file, options );
|
|
723
|
+
|
|
724
|
+
const invoiceMonth = invoiceInfo.billingDate ?
|
|
725
|
+
dayjs( invoiceInfo.billingDate ).format( 'MMMM' ) :
|
|
726
|
+
( invoiceInfo.monthOfbilling ? dayjs( invoiceInfo.monthOfbilling, 'MM' ).format( 'MMMM' ) : '' );
|
|
727
|
+
const brandName = getgroup?.registeredCompanyName || invoiceInfo.companyName || '';
|
|
728
|
+
const filename = `${invoiceInfo.invoice}-${invoiceMonth}-${brandName}.pdf`
|
|
729
|
+
.replace( /[\r\n"\\]/g, '' )
|
|
730
|
+
.trim();
|
|
731
|
+
|
|
732
|
+
return { pdfBuffer, filename };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export async function invoiceDownloadBulk( req, res ) {
|
|
736
|
+
try {
|
|
737
|
+
const invoiceIds = req.body?.invoiceIds;
|
|
738
|
+
if ( !Array.isArray( invoiceIds ) || invoiceIds.length === 0 ) {
|
|
739
|
+
return res.sendError( 'invoiceIds must be a non-empty array', 400 );
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let brandSlug = 'invoices';
|
|
743
|
+
try {
|
|
744
|
+
const firstInvoice = await invoiceService.findOne( { _id: invoiceIds[0] } );
|
|
745
|
+
if ( firstInvoice ) {
|
|
746
|
+
const firstClient = await clientService.findOne( { clientId: firstInvoice.clientId } );
|
|
747
|
+
if ( firstClient?.clientName ) {
|
|
748
|
+
brandSlug = firstClient.clientName
|
|
749
|
+
.toLowerCase()
|
|
750
|
+
.replace( /[^a-z0-9]+/g, '-' )
|
|
751
|
+
.replace( /^-|-$/g, '' ) || 'invoices';
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} catch ( e ) {
|
|
755
|
+
logger.error( { error: e, function: 'invoiceDownloadBulk.brandSlug' } );
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const zipFilename = `invoices-${brandSlug}-${dayjs().format( 'YYYY-MM-DD' )}.zip`;
|
|
759
|
+
res.set( 'Content-Type', 'application/zip' );
|
|
760
|
+
res.set( 'Content-Disposition', `attachment; filename="${zipFilename}"` );
|
|
761
|
+
|
|
762
|
+
const archive = archiver( 'zip', { zlib: { level: 6 } } );
|
|
763
|
+
archive.on( 'error', ( err ) => {
|
|
764
|
+
logger.error( { error: err, function: 'invoiceDownloadBulk.archiver' } );
|
|
765
|
+
archive.abort();
|
|
766
|
+
if ( !res.headersSent ) {
|
|
767
|
+
return res.sendError( err, 500 );
|
|
768
|
+
}
|
|
769
|
+
res.destroy( err );
|
|
770
|
+
} );
|
|
771
|
+
archive.pipe( res );
|
|
772
|
+
|
|
773
|
+
const errors = [];
|
|
774
|
+
for ( const id of invoiceIds ) {
|
|
775
|
+
try {
|
|
776
|
+
const { pdfBuffer, filename } = await buildInvoicePdfBuffer( id );
|
|
777
|
+
archive.append( pdfBuffer, { name: filename } );
|
|
778
|
+
} catch ( err ) {
|
|
779
|
+
logger.error( { error: err, function: 'invoiceDownloadBulk', invoiceId: id } );
|
|
780
|
+
errors.push( { id, reason: err?.message || String( err ) } );
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if ( errors.length > 0 ) {
|
|
785
|
+
const errorsText = errors
|
|
786
|
+
.map( ( e ) => `${e.id}\t${e.reason}` )
|
|
787
|
+
.join( '\n' );
|
|
788
|
+
archive.append(
|
|
789
|
+
`The following invoices could not be included in this archive:\n\nID\tReason\n${errorsText}\n`,
|
|
790
|
+
{ name: 'errors.txt' },
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await archive.finalize();
|
|
795
|
+
} catch ( error ) {
|
|
796
|
+
logger.error( { error: error, function: 'invoiceDownloadBulk' } );
|
|
797
|
+
if ( !res.headersSent ) {
|
|
798
|
+
return res.sendError( error, 500 );
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function invoiceAnnexure( req, res ) {
|
|
804
|
+
try {
|
|
805
|
+
const invoiceInfo = await invoiceService.findOne( { _id: req.params.invoiceId } );
|
|
806
|
+
if ( !invoiceInfo ) {
|
|
807
|
+
return res.sendError( 'Invoice not found', 404 );
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let getgroup;
|
|
811
|
+
if ( invoiceInfo.groupId ) {
|
|
812
|
+
getgroup = await billingService.findOne( { _id: invoiceInfo.groupId } );
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if ( !getgroup?.attachAnnexure ) {
|
|
816
|
+
return res.sendSuccess( { data: [] } );
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
|
|
820
|
+
const currentMonthDays = dayjs().daysInMonth();
|
|
821
|
+
|
|
822
|
+
const annexureData = await dailyPricingService.aggregate( [
|
|
823
|
+
{ $match: { clientId: invoiceInfo.clientId } },
|
|
824
|
+
{ $sort: { dateISO: -1 } },
|
|
825
|
+
{ $limit: 1 },
|
|
826
|
+
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ?? [] ] } } } } },
|
|
827
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
828
|
+
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
829
|
+
{ $project: {
|
|
830
|
+
productName: '$stores.products.productName',
|
|
831
|
+
storeId: '$stores.storeId',
|
|
832
|
+
storeName: '$stores.storeName',
|
|
833
|
+
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
834
|
+
workingdays: '$stores.products.workingdays',
|
|
835
|
+
currencyType: { $literal: symbolFor( clientDetails?.paymentInvoice?.currencyType ) },
|
|
836
|
+
} },
|
|
837
|
+
{ $sort: { productName: 1, workingdays: -1 } },
|
|
838
|
+
{ $match: { workingdays: { $gt: 0 } } },
|
|
839
|
+
{ $lookup: {
|
|
840
|
+
from: 'basepricings',
|
|
841
|
+
let: { clientId: invoiceInfo.clientId },
|
|
842
|
+
pipeline: [
|
|
843
|
+
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
844
|
+
{ $project: { standard: 1, step: 1 } },
|
|
845
|
+
],
|
|
846
|
+
as: 'basepricing',
|
|
847
|
+
} },
|
|
848
|
+
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
849
|
+
{ $project: {
|
|
850
|
+
productName: 1,
|
|
851
|
+
workingdays: 1,
|
|
852
|
+
storeName: 1,
|
|
853
|
+
currencyType: 1,
|
|
854
|
+
edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
|
|
855
|
+
storeId: 1,
|
|
856
|
+
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
857
|
+
step: '$basepricing.step',
|
|
858
|
+
} },
|
|
859
|
+
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
860
|
+
{ $project: {
|
|
861
|
+
productName: {
|
|
862
|
+
$concat: [
|
|
863
|
+
{ $toUpper: { $substr: [ '$productName', 0, 1 ] } },
|
|
864
|
+
{ $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] },
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
currencyType: 1,
|
|
868
|
+
workingdays: 1,
|
|
869
|
+
storeName: 1,
|
|
870
|
+
edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
|
|
871
|
+
storeId: 1,
|
|
872
|
+
period: { $cond: { if: { $lt: [ '$workingdays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
|
|
873
|
+
standardPrice: '$standard.negotiatePrice',
|
|
874
|
+
runningCost: { $round: [ { $multiply: [ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] }, '$workingdays' ] }, 2 ] },
|
|
875
|
+
} },
|
|
876
|
+
] );
|
|
877
|
+
|
|
878
|
+
return res.sendSuccess( { data: annexureData } );
|
|
879
|
+
} catch ( error ) {
|
|
880
|
+
logger.error( { error: error, function: 'invoiceAnnexure', invoiceId: req.params.invoiceId } );
|
|
881
|
+
return res.sendError( error, 500 );
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
496
885
|
function inWords( num ) {
|
|
497
886
|
let a = [ '', 'one ', 'two ', 'three ', 'four ', 'five ', 'six ', 'seven ', 'eight ', 'nine ', 'ten ', 'eleven ', 'twelve ', 'thirteen ', 'fourteen ', 'fifteen ', 'sixteen ', 'seventeen ', 'eighteen ', 'nineteen ' ]; let b = [ '', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety' ];
|
|
498
887
|
if ( ( num = num.toString() ).length > 9 ) return 'overflow';
|
|
@@ -607,7 +996,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
607
996
|
},
|
|
608
997
|
storeCount: { $sum: 1 },
|
|
609
998
|
totalZoneCount: { $sum: '$zoneCount' },
|
|
610
|
-
|
|
999
|
+
totalTrafficCameraCount: { $sum: '$trafficCameraCount' },
|
|
1000
|
+
totalZoneCameraCount: { $sum: '$zoneCameraCount' },
|
|
611
1001
|
},
|
|
612
1002
|
},
|
|
613
1003
|
{
|
|
@@ -617,7 +1007,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
617
1007
|
workingdays: '$_id.workingdays',
|
|
618
1008
|
storeCount: '$storeCount',
|
|
619
1009
|
totalZoneCount: '$totalZoneCount',
|
|
620
|
-
|
|
1010
|
+
totalTrafficCameraCount: '$totalTrafficCameraCount',
|
|
1011
|
+
totalZoneCameraCount: '$totalZoneCameraCount',
|
|
621
1012
|
},
|
|
622
1013
|
},
|
|
623
1014
|
{
|
|
@@ -651,7 +1042,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
651
1042
|
workingdays: 1,
|
|
652
1043
|
storeCount: 1,
|
|
653
1044
|
totalZoneCount: 1,
|
|
654
|
-
|
|
1045
|
+
totalTrafficCameraCount: 1,
|
|
1046
|
+
totalZoneCameraCount: 1,
|
|
655
1047
|
standard: {
|
|
656
1048
|
$filter: {
|
|
657
1049
|
input: '$basepricing.standard',
|
|
@@ -678,7 +1070,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
678
1070
|
},
|
|
679
1071
|
storeCount: 1,
|
|
680
1072
|
totalZoneCount: 1,
|
|
681
|
-
|
|
1073
|
+
totalTrafficCameraCount: 1,
|
|
1074
|
+
totalZoneCameraCount: 1,
|
|
682
1075
|
standardPrice: '$standard.negotiatePrice',
|
|
683
1076
|
runningCost: {
|
|
684
1077
|
$round: [
|
|
@@ -725,7 +1118,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
725
1118
|
},
|
|
726
1119
|
storeCount: { $sum: '$storeCount' },
|
|
727
1120
|
totalZoneCount: { $sum: '$totalZoneCount' },
|
|
728
|
-
|
|
1121
|
+
totalTrafficCameraCount: { $sum: '$totalTrafficCameraCount' },
|
|
1122
|
+
totalZoneCameraCount: { $sum: '$totalZoneCameraCount' },
|
|
729
1123
|
amount: { $sum: '$perstorecost' },
|
|
730
1124
|
},
|
|
731
1125
|
},
|
|
@@ -739,7 +1133,8 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
739
1133
|
},
|
|
740
1134
|
storeCount: 1,
|
|
741
1135
|
totalZoneCount: 1,
|
|
742
|
-
|
|
1136
|
+
totalTrafficCameraCount: 1,
|
|
1137
|
+
totalZoneCameraCount: 1,
|
|
743
1138
|
amount: 1,
|
|
744
1139
|
description: {
|
|
745
1140
|
$cond: {
|
|
@@ -843,8 +1238,12 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
843
1238
|
if ( store.productName === 'tangoZone' ) {
|
|
844
1239
|
if ( productBillingType === 'perZone' && store.zoneCount > 0 ) {
|
|
845
1240
|
storeCount = store.zoneCount;
|
|
846
|
-
} else if ( productBillingType === 'perCamera' && store.
|
|
847
|
-
storeCount = store.
|
|
1241
|
+
} else if ( productBillingType === 'perCamera' && store.zoneCameraCount > 0 ) {
|
|
1242
|
+
storeCount = store.zoneCameraCount;
|
|
1243
|
+
}
|
|
1244
|
+
} else if ( store.productName === 'tangoTraffic' ) {
|
|
1245
|
+
if ( productBillingType === 'perCamera' && store.trafficCameraCount > 0 ) {
|
|
1246
|
+
storeCount = store.trafficCameraCount;
|
|
848
1247
|
}
|
|
849
1248
|
}
|
|
850
1249
|
|
|
@@ -872,20 +1271,26 @@ async function standardPrice( group, getClient, baseDate ) {
|
|
|
872
1271
|
// Filter out eachStore products from aggregated results
|
|
873
1272
|
products = products.filter( ( p ) => !eachStoreProductNames.includes( p.productName ) );
|
|
874
1273
|
|
|
875
|
-
// Adjust storeCount based on billingType for tangoZone (overallStore products only)
|
|
1274
|
+
// Adjust storeCount based on billingType for tangoZone and tangoTraffic (overallStore products only)
|
|
876
1275
|
products = products.map( ( product ) => {
|
|
877
1276
|
let productBillingType = billingTypeMap[product.productName] || 'perStore';
|
|
878
1277
|
if ( product.productName === 'tangoZone' ) {
|
|
879
1278
|
if ( productBillingType === 'perZone' && product.totalZoneCount > 0 ) {
|
|
880
1279
|
product.amount = product.price * product.totalZoneCount;
|
|
881
1280
|
product.storeCount = product.totalZoneCount;
|
|
882
|
-
} else if ( productBillingType === 'perCamera' && product.
|
|
883
|
-
product.amount = product.price * product.
|
|
884
|
-
product.storeCount = product.
|
|
1281
|
+
} else if ( productBillingType === 'perCamera' && product.totalZoneCameraCount > 0 ) {
|
|
1282
|
+
product.amount = product.price * product.totalZoneCameraCount;
|
|
1283
|
+
product.storeCount = product.totalZoneCameraCount;
|
|
1284
|
+
}
|
|
1285
|
+
} else if ( product.productName === 'tangoTraffic' ) {
|
|
1286
|
+
if ( productBillingType === 'perCamera' && product.totalTrafficCameraCount > 0 ) {
|
|
1287
|
+
product.amount = product.price * product.totalTrafficCameraCount;
|
|
1288
|
+
product.storeCount = product.totalTrafficCameraCount;
|
|
885
1289
|
}
|
|
886
1290
|
}
|
|
887
1291
|
delete product.totalZoneCount;
|
|
888
|
-
delete product.
|
|
1292
|
+
delete product.totalTrafficCameraCount;
|
|
1293
|
+
delete product.totalZoneCameraCount;
|
|
889
1294
|
return product;
|
|
890
1295
|
} );
|
|
891
1296
|
|
|
@@ -991,7 +1396,8 @@ async function stepPrice( group, getClient ) {
|
|
|
991
1396
|
},
|
|
992
1397
|
storeCount: { $sum: 1 },
|
|
993
1398
|
totalZoneCount: { $sum: '$zoneCount' },
|
|
994
|
-
|
|
1399
|
+
totalTrafficCameraCount: { $sum: '$trafficCameraCount' },
|
|
1400
|
+
totalZoneCameraCount: { $sum: '$zoneCameraCount' },
|
|
995
1401
|
},
|
|
996
1402
|
},
|
|
997
1403
|
{
|
|
@@ -1001,7 +1407,8 @@ async function stepPrice( group, getClient ) {
|
|
|
1001
1407
|
workingdays: '$_id.workingdays',
|
|
1002
1408
|
storeCount: '$storeCount',
|
|
1003
1409
|
totalZoneCount: '$totalZoneCount',
|
|
1004
|
-
|
|
1410
|
+
totalTrafficCameraCount: '$totalTrafficCameraCount',
|
|
1411
|
+
totalZoneCameraCount: '$totalZoneCameraCount',
|
|
1005
1412
|
},
|
|
1006
1413
|
},
|
|
1007
1414
|
{
|
|
@@ -1083,8 +1490,12 @@ async function stepPrice( group, getClient ) {
|
|
|
1083
1490
|
if ( store.productName === 'tangoZone' ) {
|
|
1084
1491
|
if ( productBillingType === 'perZone' && store.zoneCount > 0 ) {
|
|
1085
1492
|
storeCount = store.zoneCount;
|
|
1086
|
-
} else if ( productBillingType === 'perCamera' && store.
|
|
1087
|
-
storeCount = store.
|
|
1493
|
+
} else if ( productBillingType === 'perCamera' && store.zoneCameraCount > 0 ) {
|
|
1494
|
+
storeCount = store.zoneCameraCount;
|
|
1495
|
+
}
|
|
1496
|
+
} else if ( store.productName === 'tangoTraffic' ) {
|
|
1497
|
+
if ( productBillingType === 'perCamera' && store.trafficCameraCount > 0 ) {
|
|
1498
|
+
storeCount = store.trafficCameraCount;
|
|
1088
1499
|
}
|
|
1089
1500
|
}
|
|
1090
1501
|
|
|
@@ -1114,18 +1525,23 @@ async function stepPrice( group, getClient ) {
|
|
|
1114
1525
|
// Filter out eachStore products from aggregated results
|
|
1115
1526
|
products = products.filter( ( p ) => !eachStoreProductNames.includes( p.productName ) );
|
|
1116
1527
|
|
|
1117
|
-
// Adjust storeCount based on billingType for tangoZone (overallStore only)
|
|
1528
|
+
// Adjust storeCount based on billingType for tangoZone and tangoTraffic (overallStore only)
|
|
1118
1529
|
products = products.map( ( product ) => {
|
|
1119
1530
|
let productBillingType = billingTypeMap[product.productName] || 'perStore';
|
|
1120
1531
|
if ( product.productName === 'tangoZone' ) {
|
|
1121
1532
|
if ( productBillingType === 'perZone' && product.totalZoneCount > 0 ) {
|
|
1122
1533
|
product.storeCount = product.totalZoneCount;
|
|
1123
|
-
} else if ( productBillingType === 'perCamera' && product.
|
|
1124
|
-
product.storeCount = product.
|
|
1534
|
+
} else if ( productBillingType === 'perCamera' && product.totalZoneCameraCount > 0 ) {
|
|
1535
|
+
product.storeCount = product.totalZoneCameraCount;
|
|
1536
|
+
}
|
|
1537
|
+
} else if ( product.productName === 'tangoTraffic' ) {
|
|
1538
|
+
if ( productBillingType === 'perCamera' && product.totalTrafficCameraCount > 0 ) {
|
|
1539
|
+
product.storeCount = product.totalTrafficCameraCount;
|
|
1125
1540
|
}
|
|
1126
1541
|
}
|
|
1127
1542
|
delete product.totalZoneCount;
|
|
1128
|
-
delete product.
|
|
1543
|
+
delete product.totalTrafficCameraCount;
|
|
1544
|
+
delete product.totalZoneCameraCount;
|
|
1129
1545
|
return product;
|
|
1130
1546
|
} );
|
|
1131
1547
|
|
|
@@ -1700,6 +2116,13 @@ export async function updateInvoice( req, res ) {
|
|
|
1700
2116
|
}
|
|
1701
2117
|
} );
|
|
1702
2118
|
|
|
2119
|
+
// Guard: updateInvoice is an admin override that can set arbitrary status
|
|
2120
|
+
// values (including skipping pipeline stages or moving backward). Reject
|
|
2121
|
+
// values outside the enum to keep the DB consistent.
|
|
2122
|
+
if ( updateData.status && !invoiceStatusEnum.includes( updateData.status ) ) {
|
|
2123
|
+
return res.sendError( `Invalid status value: ${updateData.status}. Must be one of: ${invoiceStatusEnum.join( ', ' )}`, 400 );
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1703
2126
|
if ( req.body.billingDate ) {
|
|
1704
2127
|
updateData.createdAt = new Date( req.body.billingDate );
|
|
1705
2128
|
}
|
|
@@ -1773,3 +2196,74 @@ export async function getClientBasePricing( req, res ) {
|
|
|
1773
2196
|
}
|
|
1774
2197
|
}
|
|
1775
2198
|
|
|
2199
|
+
export async function deleteInvoice( req, res ) {
|
|
2200
|
+
try {
|
|
2201
|
+
const { invoiceId } = req.params;
|
|
2202
|
+
if ( !invoiceId ) {
|
|
2203
|
+
return res.sendError( 'Invoice ID is required', 400 );
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
const invoice = await invoiceService.findOne( { _id: invoiceId } );
|
|
2207
|
+
if ( !invoice ) {
|
|
2208
|
+
return res.sendError( 'Invoice not found', 404 );
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
await invoiceService.deleteRecord( { _id: invoiceId } );
|
|
2212
|
+
|
|
2213
|
+
res.sendSuccess( { message: 'Invoice deleted successfully' } );
|
|
2214
|
+
} catch ( error ) {
|
|
2215
|
+
logger.error( { error: error, function: 'deleteInvoice' } );
|
|
2216
|
+
return res.sendError( error, 500 );
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
async function transitionInvoiceStatus( req, res, fromStatus, toStatus ) {
|
|
2221
|
+
try {
|
|
2222
|
+
const { invoiceId } = req.body;
|
|
2223
|
+
|
|
2224
|
+
const invoice = await invoiceService.findOne( { _id: invoiceId } );
|
|
2225
|
+
if ( !invoice ) {
|
|
2226
|
+
return res.sendError( 'Invoice not found', 404 );
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if ( invoice.status !== fromStatus ) {
|
|
2230
|
+
return res.sendError(
|
|
2231
|
+
`Invoice is currently at status '${invoice.status}', not '${fromStatus}'. Another user may have advanced it.`,
|
|
2232
|
+
409,
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
await invoiceService.updateOne( { _id: invoiceId }, { status: toStatus } );
|
|
2237
|
+
|
|
2238
|
+
insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, {
|
|
2239
|
+
userName: req.user?.userName,
|
|
2240
|
+
email: req.user?.email,
|
|
2241
|
+
clientId: invoice.clientId,
|
|
2242
|
+
logSubType: 'invoiceStatusTransition',
|
|
2243
|
+
logType: 'invoice',
|
|
2244
|
+
date: new Date(),
|
|
2245
|
+
changes: [ `Invoice ${invoice.invoice} advanced from ${fromStatus} to ${toStatus} by ${req.user?.email}` ],
|
|
2246
|
+
eventType: '',
|
|
2247
|
+
timestamp: new Date(),
|
|
2248
|
+
showTo: [ 'tango' ],
|
|
2249
|
+
} );
|
|
2250
|
+
|
|
2251
|
+
return res.sendSuccess( { invoiceId, fromStatus, status: toStatus } );
|
|
2252
|
+
} catch ( error ) {
|
|
2253
|
+
logger.error( { error: error, function: 'transitionInvoiceStatus', fromStatus, toStatus, invoiceId: req.body?.invoiceId } );
|
|
2254
|
+
return res.sendError( error, 500 );
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
export async function approveInvoiceCsm( req, res ) {
|
|
2259
|
+
return transitionInvoiceStatus( req, res, 'pendingCsm', 'pendingFinance' );
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
export async function approveInvoiceFinance( req, res ) {
|
|
2263
|
+
return transitionInvoiceStatus( req, res, 'pendingFinance', 'pendingApproval' );
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
export async function approveInvoiceApproval( req, res ) {
|
|
2267
|
+
return transitionInvoiceStatus( req, res, 'pendingApproval', 'approved' );
|
|
2268
|
+
}
|
|
2269
|
+
|