tango-app-api-payment-subscription 3.0.60-dev → 3.0.61-dev

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.
@@ -0,0 +1,867 @@
1
+ import * as invoiceService from '../services/invoice.service.js';
2
+ import * as dailyPricingService from '../services/dailyPrice.service.js';
3
+ import * as clientService from '../services/clientPayment.services.js';
4
+ import * as billingService from '../services/billing.service.js';
5
+ import dayjs from 'dayjs';
6
+ import { logger, checkFileExist, signedUrl } from 'tango-app-api-middleware';
7
+ import Handlebars from 'handlebars';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import htmlpdf from 'html-pdf-node';
11
+ import basePricingModel from 'tango-api-schema/schema/basePricing.model.js';
12
+ export async function createInvoice( req, res ) {
13
+ try {
14
+ if ( req.body.clientList.length > 0 ) {
15
+ req.body.clientList = req.body.clientList;
16
+ } else {
17
+ let clients = await clientService.find( { status: 'active' } );
18
+ req.body.clientList = clients.map( ( client ) => client.clientId );
19
+ }
20
+ for ( let client of req.body.clientList ) {
21
+ let invoiceGroup = await billingService.aggregatebilling( [
22
+ {
23
+ $match: {
24
+ clientId: client,
25
+ },
26
+ },
27
+ ] );
28
+ for ( let group of invoiceGroup ) {
29
+ let Finacialyear = getCurrentFinancialYear();
30
+ let previousinvoice = await invoiceService.findandsort( {}, {}, { invoiceIndex: -1 } );
31
+ let invoiceNo = '00001';
32
+ if ( previousinvoice && previousinvoice.length > 0 ) {
33
+ invoiceNo = Number( previousinvoice[0].invoiceIndex ) + 1;
34
+ invoiceNo = invoiceNo.toString().padStart( 5, '0' );
35
+ }
36
+
37
+ let address = group.addressLineOne + group.addressLineTwo + group.city + ',' + group.state + ',' + group.country + ' -' + group.pinCode;
38
+ let getClient = await clientService.findOne( { clientId: client, status: 'active' } );
39
+ let products;
40
+ if ( getClient.priceType === 'standard' ) {
41
+ products = await standardPrice( client, group );
42
+ } else {
43
+ products = await stepPrice( client, group, res );
44
+ }
45
+ let amount = products.reduce( ( sum, product ) => sum + product.amount, 0 );
46
+ let taxList = [];
47
+ let totalAmount = 0;
48
+ if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
49
+ let taxAmount = ( amount * 18 ) / 100;
50
+ totalAmount = ( amount + taxAmount ).toFixed( 2 );
51
+ taxList.push(
52
+ {
53
+ 'currency': '₹',
54
+ 'type': 'CGST',
55
+ 'value': 9,
56
+ 'taxAmount': ( ( amount * 9 ) / 100 ).toFixed( 2 ),
57
+ }, {
58
+ 'currency': '₹',
59
+ 'type': 'SGST',
60
+ 'value': 9,
61
+ 'taxAmount': ( ( amount * 9 ) / 100 ).toFixed( 2 ),
62
+ },
63
+ );
64
+ } else {
65
+ let taxAmount = ( amount * 18 ) / 100;
66
+ totalAmount = ( amount + taxAmount ).toFixed( 2 );
67
+ taxList.push(
68
+ {
69
+ 'currency': '₹',
70
+ 'type': 'IGST',
71
+ 'value': 18,
72
+ 'taxAmount': ( taxAmount ).toFixed( 2 ),
73
+ },
74
+ );
75
+ }
76
+
77
+
78
+ let totalStoreCount = products.reduce( ( sum, item ) => sum + item.storeCount, 0 );
79
+ let data = {
80
+ groupName: group.groupName,
81
+ groupId: group._id,
82
+ invoice: `INV-${Finacialyear}-${invoiceNo}`,
83
+ products: products,
84
+ status: 'pending',
85
+ amount: amount,
86
+ invoiceIndex: invoiceNo,
87
+ tax: taxList,
88
+ companyName: group.registeredCompanyName,
89
+ companyAddress: address,
90
+ PlaceOfSupply: group.placeOfSupply,
91
+ GSTNumber: group.gst,
92
+ totalAmount: totalAmount,
93
+ clientId: client,
94
+ paymentMethod: '',
95
+ billingDate: new Date(),
96
+ stores: totalStoreCount,
97
+ };
98
+
99
+ let invoiceExists = await invoiceService.findOne( { monthOfbilling: req.body.monthOfbilling, clientId: getClient.clientId } );
100
+ if ( invoiceExists ) {
101
+ logger.info( `invoice already exist for the month${req.body.monthOfbilling} for ${getClient.clientId}` );
102
+ } else {
103
+ await invoiceService.create( data );
104
+ }
105
+ }
106
+ }
107
+ res.sendSuccess( 'Invoice Created SuccessFully' );
108
+ } catch ( error ) {
109
+ logger.error( { error: error, function: 'createInvoice' } );
110
+ return res.sendError( error, 500 );
111
+ }
112
+ }
113
+ function getCurrentFinancialYear() {
114
+ const today = new Date();
115
+ const currentMonth = today.getMonth();
116
+ const currentYear = today.getFullYear();
117
+ if ( currentMonth >= 3 ) {
118
+ return currentYear.toString().slice( -2 ) + '-' + ( currentYear + 1 ).toString().slice( -2 );
119
+ } else {
120
+ return ( ( currentYear - 1 ).toString().slice( -2 ) ) + '-' + currentYear.toString().slice( -2 );
121
+ }
122
+ }
123
+
124
+
125
+ export async function invoiceDownload( req, res ) {
126
+ try {
127
+ let invoiceData;
128
+ let invoiceInfo = await invoiceService.findOne( { _id: req.params.invoiceId } );
129
+ if ( invoiceInfo ) {
130
+ let clientDetails = await clientService.findOne( { clientId: invoiceInfo.clientId } );
131
+ invoiceInfo.products.forEach( ( item, index ) => {
132
+ item.index = index + 1;
133
+ let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
134
+ firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
135
+ item.productName = firstWord + ' ' + secondWord;
136
+ item.price = item.price.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
137
+ item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
138
+ item.currency = clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹';
139
+ } );
140
+
141
+
142
+ for ( let tax of invoiceInfo.tax ) {
143
+ tax.taxAmount = tax.taxAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
144
+ }
145
+ let invoiceDate = dayjs( invoiceInfo.createdAt ).format( 'DD/MM/YYYY' );
146
+ let days = clientDetails?.paymentInvoice?.extendPaymentPeriodDays || 10;
147
+ let dueDate = invoiceInfo?.dueDate ? dayjs( invoiceInfo?.dueDate ).format( 'DD/MM/YYYY' ) : dayjs().add( days, 'days' ).format( 'DD/MM/YYYY' );
148
+
149
+ invoiceInfo.totalAmount = Math.round( invoiceInfo.totalAmount );
150
+ let AmountinWords = inWords( invoiceInfo.totalAmount );
151
+ invoiceData = {
152
+ ...invoiceInfo._doc,
153
+ clientName: clientDetails.clientName,
154
+ extendDays: clientDetails.paymentInvoice.extendPaymentPeriodDays,
155
+ address: clientDetails.billingDetails.billingAddress,
156
+ subtotal: invoiceInfo.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
157
+ companyName: invoiceInfo.companyName,
158
+ companyAddress: invoiceInfo.companyAddress,
159
+ PlaceOfSupply: invoiceInfo.PlaceOfSupply,
160
+ GSTNumber: invoiceInfo.GSTNumber,
161
+ PoNum: '',
162
+ amountwords: AmountinWords,
163
+ Terms: `Term ${clientDetails.paymentInvoice.extendPaymentPeriodDays}`,
164
+ currencyType: clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹',
165
+ totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
166
+ invoiceDate,
167
+ dueDate,
168
+ };
169
+ const currentMonthDays = dayjs().daysInMonth();
170
+ let getgroup = await billingService.findOne( { _id: invoiceInfo.groupId } );
171
+ let annuxureData = await dailyPricingService.aggregate( [
172
+ {
173
+ $match: {
174
+ clientId: invoiceInfo.clientId,
175
+ },
176
+ },
177
+ {
178
+ $sort: { dateISO: -1 },
179
+ },
180
+ { $limit: 1 },
181
+ {
182
+ $project: {
183
+ stores: {
184
+ $filter: {
185
+ input: '$stores',
186
+ as: 'item',
187
+ cond: { $in: [ '$$item.storeId', getgroup.stores ] },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ {
193
+ $unwind: {
194
+ path: '$stores',
195
+ preserveNullAndEmptyArrays: false,
196
+ },
197
+ },
198
+ {
199
+ $unwind: {
200
+ path: '$stores.products',
201
+ preserveNullAndEmptyArrays: false,
202
+ },
203
+ },
204
+ {
205
+ $project: {
206
+ productName: '$stores.products.productName',
207
+ storeId: '$stores.storeId',
208
+ storeName: '$stores.storeName',
209
+ edgefirstFileDate: '$stores.edgefirstFileDate',
210
+ workingdays: '$stores.products.workingdays',
211
+ currencyType: { $literal: clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹' },
212
+ },
213
+ },
214
+ {
215
+ $sort: {
216
+ productName: 1,
217
+ workingdays: -1,
218
+ },
219
+ },
220
+ {
221
+ $match: { workingdays: { $gt: 0 } },
222
+ },
223
+
224
+ {
225
+ $lookup: {
226
+ from: 'basepricings',
227
+ let: { clientId: invoiceInfo.clientId },
228
+ pipeline: [
229
+ {
230
+ $match: {
231
+ $expr: {
232
+ $eq: [ '$clientId', '$$clientId' ],
233
+ },
234
+ },
235
+ },
236
+ {
237
+ $project: {
238
+ standard: 1,
239
+ step: 1,
240
+ },
241
+ },
242
+ ],
243
+ as: 'basepricing',
244
+ },
245
+ },
246
+ {
247
+ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true },
248
+ },
249
+ {
250
+ $project: {
251
+ productName: 1,
252
+ workingdays: 1,
253
+ storeName: 1,
254
+ currencyType: 1,
255
+ edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
256
+ storeId: 1,
257
+ standard: {
258
+ $filter: {
259
+ input: '$basepricing.standard',
260
+ as: 'standard',
261
+ cond: { $eq: [ '$$standard.productName', '$productName' ] },
262
+ },
263
+ },
264
+ step: '$basepricing.step',
265
+ },
266
+ },
267
+ {
268
+ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true },
269
+ },
270
+ {
271
+ $project: {
272
+ productName: {
273
+ $concat: [
274
+ { $toUpper: { $substr: [ '$productName', 0, 1 ] } }, // Uppercase first letter
275
+ { $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] }, // Rest of the string
276
+ ],
277
+ },
278
+ currencyType: 1,
279
+ workingdays: 1,
280
+ storeName: 1,
281
+ edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
282
+ storeId: 1,
283
+ period: {
284
+ $cond: {
285
+ if: { $lt: [ '$workingdays', currentMonthDays ] },
286
+ then: 'prorate',
287
+ else: 'fullmonth',
288
+ },
289
+ },
290
+ standardPrice: '$standard.negotiatePrice',
291
+ runningCost: {
292
+ $round: [
293
+ {
294
+ $multiply: [
295
+ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] },
296
+ '$workingdays',
297
+ ],
298
+ },
299
+ 2,
300
+ ],
301
+ },
302
+ },
303
+ },
304
+ ],
305
+ );
306
+ invoiceData.annuxureData = annuxureData;
307
+
308
+ const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
309
+ const template = Handlebars.compile( templateHtml );
310
+ const html = template( { ...invoiceData } );
311
+ let file = {
312
+ content: html,
313
+ };
314
+ let options = {
315
+ format: 'A4', margin: {
316
+ top: '0.5in',
317
+ right: '0.5in',
318
+ bottom: '0.5in',
319
+ left: '0.5in',
320
+ },
321
+ printBackground: true,
322
+ preferCSSPageSize: true,
323
+ };
324
+ const date = dayjs( invoiceData.monthOfbilling, 'MM' );
325
+ const monthName = date.format( 'MMMM' );
326
+
327
+ htmlpdf.generatePdf( file, options ).then( async function( pdfBuffer ) {
328
+ if ( req.body.sendInvoice ) {
329
+ let mailSubject = `Invoice for ${monthName} - Tango/${clientDetails.clientName}`;
330
+ let mailbody = `<div>Dear Team,</div>
331
+ <div style="margin-top:10px">
332
+ Please find attached the Invoice for the month of ${monthName}'24.
333
+ </div>
334
+ <div style="margin-top:10px">
335
+ Best Regards,
336
+ </div>
337
+ <div style="margin-top:5px">
338
+ Tango Finance
339
+ </div>`;
340
+
341
+ let filename = `${clientDetails.clientName}-${invoiceData.invoice}-${monthName}.pdf`;
342
+ let attachments = {
343
+ filename: `${filename}`,
344
+ content: pdfBuffer,
345
+ contentType: 'application/pdf', // e.g., 'application/pdf'
346
+ };
347
+ clientDetails.paymentInvoice.invoiceCC = [ ...clientDetails.paymentInvoice.invoiceCC, ...[ 'sireesha@tangotech.co.in' ] ];
348
+ const result = await sendEmailWithSES( clientDetails.paymentInvoice.invoiceTo, mailSubject, mailbody, attachments, 'no-reply@tangotech.ai', clientDetails.paymentInvoice.invoiceCC );
349
+ if ( result ) {
350
+ await invoiceService.updateOne( { _id: req.params.invoiceId }, { status: req.body.status } );
351
+ return res.sendSuccess( result );
352
+ }
353
+ }
354
+ res.set( 'Content-Disposition', 'attachment; filename="generated-pdf.pdf"' );
355
+ res.set( 'Content-Type', 'application/pdf' );
356
+ res.send( pdfBuffer );
357
+ } );
358
+ return;
359
+ }
360
+ } catch ( error ) {
361
+ logger.error( { error: error, function: 'invoiceDownload' } );
362
+ return res.sendError( error, 500 );
363
+ }
364
+ }
365
+ function inWords( num ) {
366
+ 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' ];
367
+ if ( ( num = num.toString() ).length > 9 ) return 'overflow';
368
+ let n = ( '000000000' + num ).substr( -9 ).match( /^(\d{2})(\d{2})(\d{2})(\d{1})(\d{2})$/ );
369
+ if ( !n ) return; let str = '';
370
+ str += ( n[1] != 0 ) ? ( a[Number( n[1] )] || b[n[1][0]] + ' ' + a[n[1][1]] ) + 'crore ' : '';
371
+ str += ( n[2] != 0 ) ? ( a[Number( n[2] )] || b[n[2][0]] + ' ' + a[n[2][1]] ) + 'lakh ' : '';
372
+ str += ( n[3] != 0 ) ? ( a[Number( n[3] )] || b[n[3][0]] + ' ' + a[n[3][1]] ) + 'thousand ' : '';
373
+ str += ( n[4] != 0 ) ? ( a[Number( n[4] )] || b[n[4][0]] + ' ' + a[n[4][1]] ) + 'hundred ' : '';
374
+ str += ( n[5] != 0 ) ? ( ( str != '' ) ? 'and ' : '' ) + ( a[Number( n[5] )] || b[n[5][0]] + ' ' + a[n[5][1]] ) : '';
375
+
376
+ return str.toLowerCase().split( ' ' ).map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ).join( ' ' );
377
+ }
378
+
379
+
380
+ async function standardPrice( client, group ) {
381
+ const currentMonthDays = dayjs().daysInMonth();
382
+ let products = await dailyPricingService.aggregate( [
383
+ {
384
+ $match: {
385
+ clientId: client,
386
+ },
387
+ },
388
+ {
389
+ $sort: { dateISO: -1 },
390
+ },
391
+ { $limit: 1 },
392
+ {
393
+ $project: {
394
+ stores: {
395
+ $filter: {
396
+ input: '$stores',
397
+ as: 'item',
398
+ cond: { $in: [ '$$item.storeId', group.stores ] },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ {
404
+ $unwind: {
405
+ path: '$stores',
406
+ preserveNullAndEmptyArrays: false,
407
+ },
408
+ },
409
+ {
410
+ $unwind: {
411
+ path: '$stores.products',
412
+ preserveNullAndEmptyArrays: false,
413
+ },
414
+ },
415
+ {
416
+ $project: {
417
+ productName: '$stores.products.productName',
418
+ storeId: '$stores.storeId',
419
+ workingDays: '$stores.products.workingdays',
420
+ storeStatus: '$stores.status',
421
+ },
422
+ },
423
+ {
424
+ $match: {
425
+ workingDays: { $gt: 0 },
426
+ },
427
+ },
428
+ {
429
+ $project: {
430
+ productName: 1,
431
+ storeId: 1,
432
+ prorate: { $literal: group.proRata },
433
+ workingDays: 1,
434
+ storeStatus: 1,
435
+ },
436
+ },
437
+ {
438
+ $project: {
439
+ productName: 1,
440
+ storeId: 1,
441
+ workingDays: {
442
+ $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
443
+ },
444
+ storeStatus: 1,
445
+ },
446
+ },
447
+ {
448
+ $group: {
449
+ _id: {
450
+ productName: '$productName',
451
+ storeId: '$storeId',
452
+ },
453
+ workingdays: { $first: '$workingDays' },
454
+ },
455
+ },
456
+ {
457
+ $group: {
458
+ _id: {
459
+ productName: '$_id.productName',
460
+ workingdays: '$workingdays',
461
+ },
462
+ storeCount: { $sum: 1 },
463
+ },
464
+ },
465
+ {
466
+ $project: {
467
+ _id: 0,
468
+ productName: '$_id.productName',
469
+ workingdays: '$_id.workingdays',
470
+ storeCount: '$storeCount',
471
+ },
472
+ },
473
+ {
474
+ $lookup: {
475
+ from: 'basepricings',
476
+ let: { clientId: client },
477
+ pipeline: [
478
+ {
479
+ $match: {
480
+ $expr: {
481
+ $eq: [ '$clientId', '$$clientId' ],
482
+ },
483
+ },
484
+ },
485
+ {
486
+ $project: {
487
+ standard: 1,
488
+ step: 1,
489
+ },
490
+ },
491
+ ],
492
+ as: 'basepricing',
493
+ },
494
+ },
495
+ {
496
+ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true },
497
+ },
498
+ {
499
+ $project: {
500
+ productName: 1,
501
+ workingdays: 1,
502
+ storeCount: 1,
503
+ standard: {
504
+ $filter: {
505
+ input: '$basepricing.standard',
506
+ as: 'standard',
507
+ cond: { $eq: [ '$$standard.productName', '$productName' ] },
508
+ },
509
+ },
510
+ step: '$basepricing.step',
511
+ },
512
+ },
513
+ {
514
+ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true },
515
+ },
516
+ {
517
+ $project: {
518
+ productName: 1,
519
+ workingdays: 1,
520
+ period: {
521
+ $cond: {
522
+ if: { $lt: [ '$workingdays', currentMonthDays ] },
523
+ then: 'prorate',
524
+ else: 'fullmonth',
525
+ },
526
+ },
527
+ storeCount: 1,
528
+ standardPrice: '$standard.negotiatePrice',
529
+ runningCost: {
530
+ $round: [
531
+ {
532
+ $multiply: [
533
+ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] },
534
+ '$workingdays',
535
+ ],
536
+ },
537
+ 2,
538
+ ],
539
+ },
540
+ perstorecost: {
541
+ $cond: {
542
+ if: { $eq: [ '$workingdays', currentMonthDays ] },
543
+ then: {
544
+ $multiply: [ '$standard.negotiatePrice', '$storeCount' ],
545
+ },
546
+ else: {
547
+ $round: [
548
+ {
549
+ $multiply: [
550
+ {
551
+ $multiply: [
552
+ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] },
553
+ '$workingdays',
554
+ ],
555
+ },
556
+ '$storeCount',
557
+ ],
558
+ },
559
+ 2,
560
+ ],
561
+ },
562
+ },
563
+ },
564
+ },
565
+ },
566
+ {
567
+ $group: {
568
+ _id: {
569
+ productName: '$productName',
570
+ period: '$period',
571
+ },
572
+ storeCount: { $sum: '$storeCount' },
573
+ amount: { $sum: '$perstorecost' },
574
+ },
575
+ },
576
+ {
577
+ $project: {
578
+ _id: 0,
579
+ productName: '$_id.productName',
580
+ period: '$_id.period',
581
+ price: {
582
+ $round: [ { $divide: [ '$amount', '$storeCount' ] }, 2 ],
583
+ },
584
+ storeCount: 1,
585
+ amount: 1,
586
+ description: {
587
+ $cond: {
588
+ if: { $eq: [ '$_id.productName', 'tangoZone' ] },
589
+ then: 'Product category/section analytics',
590
+ else: 'Customer Footfall Analytics',
591
+ },
592
+ },
593
+ HsnNumber: '998314',
594
+ month: { $literal: dayjs().format( 'MMM YYYY' ) },
595
+ },
596
+ },
597
+ ] );
598
+
599
+
600
+ return products;
601
+ }
602
+
603
+
604
+ async function stepPrice( client, group, res ) {
605
+ const currentMonthDays = dayjs().daysInMonth();
606
+ let products = await dailyPricingService.aggregate( [
607
+ {
608
+ $match: {
609
+ clientId: client,
610
+ },
611
+ },
612
+ {
613
+ $sort: { dateISO: -1 },
614
+ },
615
+ { $limit: 1 },
616
+ {
617
+ $project: {
618
+ stores: {
619
+ $filter: {
620
+ input: '$stores',
621
+ as: 'item',
622
+ cond: { $in: [ '$$item.storeId', group.stores ] },
623
+ },
624
+ },
625
+ },
626
+ },
627
+ {
628
+ $unwind: {
629
+ path: '$stores',
630
+ preserveNullAndEmptyArrays: false,
631
+ },
632
+ },
633
+ {
634
+ $unwind: {
635
+ path: '$stores.products',
636
+ preserveNullAndEmptyArrays: false,
637
+ },
638
+ },
639
+ {
640
+ $project: {
641
+ productName: '$stores.products.productName',
642
+ storeId: '$stores.storeId',
643
+ workingDays: '$stores.products.workingdays',
644
+ storeStatus: '$stores.status',
645
+ },
646
+ },
647
+ {
648
+ $match: {
649
+ workingDays: { $gt: 0 },
650
+ },
651
+ },
652
+ {
653
+ $project: {
654
+ productName: 1,
655
+ storeId: 1,
656
+ prorate: { $literal: group.proRata },
657
+ workingDays: 1,
658
+ storeStatus: 1,
659
+ },
660
+ },
661
+ {
662
+ $project: {
663
+ productName: 1,
664
+ storeId: 1,
665
+ workingDays: {
666
+ $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
667
+ },
668
+ storeStatus: 1,
669
+ },
670
+ }, {
671
+ $group: {
672
+ _id: {
673
+ productName: '$productName',
674
+ storeId: '$storeId',
675
+ },
676
+ workingdays: { $first: '$workingDays' },
677
+ },
678
+ },
679
+ {
680
+ $group: {
681
+ _id: {
682
+ productName: '$_id.productName',
683
+ workingdays: '$workingdays',
684
+ },
685
+ storeCount: { $sum: 1 },
686
+ },
687
+ },
688
+ {
689
+ $project: {
690
+ _id: 0,
691
+ productName: '$_id.productName',
692
+ workingdays: '$_id.workingdays',
693
+ storeCount: '$storeCount',
694
+ },
695
+ },
696
+ {
697
+ $sort: {
698
+ workingdays: -1,
699
+ },
700
+ },
701
+
702
+
703
+ ] );
704
+ let stepPrice = await basePricingModel.findOne( { clientId: client } );
705
+ let data = products;
706
+ let pricing = stepPrice.step;
707
+
708
+ const applyPricing = ( data, pricing ) => {
709
+ let totalcount = 0;
710
+ return data.map( ( item ) => {
711
+ totalcount = totalcount + item.storeCount;
712
+
713
+
714
+ let applicablePricing = pricing.find( ( price ) => {
715
+ const [ minRange, maxRange ] = price.storeRange.split( '-' ).map( Number );
716
+ return totalcount >= minRange && totalcount <= maxRange;
717
+ } );
718
+ if ( item.workingdays === currentMonthDays ) {
719
+ item.period = 'fullMonth';
720
+ item.runningCost = item.storeCount * applicablePricing.negotiatePrice;
721
+ } else {
722
+ item.period = 'proRate';
723
+ item.runningCost = ( item.storeCount * ( applicablePricing.negotiatePrice / currentMonthDays ) * item.workingdays ).toFixed( 2 );
724
+ }
725
+
726
+ return {
727
+ ...item,
728
+ negotiatePrice: applicablePricing.negotiatePrice,
729
+ perstorecost: ( ( applicablePricing.negotiatePrice / currentMonthDays ) * item.workingdays ).toFixed( 2 ),
730
+ };
731
+ } );
732
+ };
733
+
734
+ const result = applyPricing( data, pricing );
735
+ const groupedData = result.reduce( ( acc, item ) => {
736
+ const { productName, period, runningCost } = item;
737
+ const key = `${productName}_${period}`;
738
+ if ( !acc[key] ) {
739
+ acc[key] = {
740
+ productName,
741
+ period,
742
+ totalRunningCost: 0,
743
+ count: 0,
744
+ };
745
+ }
746
+
747
+ acc[key].totalRunningCost += parseFloat( runningCost );
748
+ acc[key].count += item.storeCount;
749
+
750
+ return acc;
751
+ }, {} );
752
+
753
+ // Calculating average running cost
754
+ const finalresult = Object.values( groupedData ).map( ( group ) => {
755
+ let description = '';
756
+ if ( group.productName === 'tangoTraffic' ) {
757
+ description = 'Customer Footfall Analytics';
758
+ } else if ( group.productName === 'tangoZone' ) {
759
+ description = 'Product category/section analytics';
760
+ } else {
761
+ description = '';
762
+ }
763
+ return {
764
+ productName: group.productName,
765
+ period: group.period,
766
+ storeCount: group.count,
767
+ description: description,
768
+ HsnNumber: '998314',
769
+ amount: group.totalRunningCost,
770
+ month: dayjs().format( 'MMM YYYY' ),
771
+ price: ( group.totalRunningCost / group.count ).toFixed( 2 ),
772
+ };
773
+ } );
774
+
775
+ // return res.sendSuccess( finalresult );
776
+ return finalresult;
777
+ }
778
+
779
+
780
+ export async function clientInvoiceList( req, res ) {
781
+ try {
782
+ let query = [ {
783
+ $match: {
784
+ clientId: { $in: req.body.clientId },
785
+ },
786
+ }, {
787
+ $lookup: {
788
+ from: 'clients',
789
+ let: { clientId: '$clientId' },
790
+ pipeline: [
791
+ {
792
+ $match: {
793
+ $expr: {
794
+ $eq: [ '$clientId', '$$clientId' ],
795
+ },
796
+ },
797
+ },
798
+ {
799
+ $project: {
800
+ clientName: 1,
801
+ logo: '$profileDetails.logo',
802
+ currencyType: '$paymentInvoice.currencyType',
803
+ },
804
+ },
805
+ ],
806
+ as: 'clientDetails',
807
+ },
808
+ },
809
+ {
810
+ $unwind: { path: '$clientDetails', preserveNullAndEmptyArrays: true },
811
+ },
812
+ {
813
+ $project: {
814
+ clientName: '$clientDetails.clientName',
815
+ logo: '$clientDetails.logo',
816
+ currencyType: '$clientDetails.currencyType',
817
+ invoice: 1,
818
+ stores: 1,
819
+ totalAmount: 1,
820
+ groupName: 1,
821
+ status: 1,
822
+ paymentStatus: 1,
823
+ clientId: 1,
824
+ },
825
+ },
826
+ ];
827
+ if ( req.body.searchValue && req.body.searchValue != '' ) {
828
+ query.push( {
829
+ $match: {
830
+ $or: [
831
+ { groupName: { $regex: req.body.searchValue, $options: 'i' } },
832
+ { clientName: { $regex: req.body.searchValue, $options: 'i' } },
833
+ { paymentStatus: { $regex: req.body.searchValue, $options: 'i' } },
834
+ ],
835
+ },
836
+ } );
837
+ }
838
+ if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
839
+ query.push( {
840
+ $sort: { [req.body.sortColumName]: req.body.sortBy },
841
+ } );
842
+ }
843
+ let count = await invoiceService.aggregate( query );
844
+ if ( req.body.limit && req.body.offset && !req.body.export ) {
845
+ query.push(
846
+ { $skip: ( req.body.offset - 1 ) * req.body.limit },
847
+ { $limit: Number( req.body.limit ) },
848
+ );
849
+ }
850
+ let invoiceList = await invoiceService.aggregate( query );
851
+ const bucket = JSON.parse( process.env.BUCKET );
852
+ for ( let client of invoiceList ) {
853
+ const isLogoExist = await checkFileExist( { Bucket: bucket.assets, Key: `${client.clientId}/logo/${client?.logo}` } );
854
+ if ( isLogoExist ) {
855
+ client.logo = await signedUrl( { Bucket: bucket.assets, file_path: `${client.clientId}/logo/${client?.logo}` } );
856
+ } else {
857
+ client.logo = '';
858
+ }
859
+ // return client;
860
+ }
861
+
862
+ res.sendSuccess( { count: count.length, data: invoiceList } );
863
+ } catch ( error ) {
864
+ logger.error( { error: error, function: 'clientInvoiceList' } );
865
+ return res.sendError( error, 500 );
866
+ }
867
+ }