tango-app-api-payment-subscription 3.5.5 → 3.5.7
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/package.json +2 -2
- package/src/controllers/bankTransaction.controller.js +617 -0
- package/src/controllers/brandsBilling.controller.js +443 -7
- package/src/controllers/estimate.controller.js +317 -0
- package/src/controllers/invoice.controller.js +172 -260
- package/src/controllers/paymentReminder.controller.js +81 -0
- package/src/controllers/paymentSubscription.controllers.js +55 -3
- package/src/dtos/validation.dtos.js +6 -0
- package/src/hbs/estimatePdf.hbs +125 -0
- package/src/hbs/invoicePdf.hbs +27 -0
- package/src/routes/billing.routes.js +5 -0
- package/src/routes/brandsBilling.routes.js +3 -1
- package/src/routes/invoice.routes.js +18 -0
- package/src/services/bankTransaction.service.js +21 -0
- package/src/services/estimate.service.js +25 -0
- package/src/services/paymentReminder.service.js +9 -0
|
@@ -17,6 +17,7 @@ import { symbolFor } from '../utils/currency.js';
|
|
|
17
17
|
import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
|
|
18
18
|
import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
|
|
19
19
|
import * as assignedStoreService from '../services/assignedStore.service.js';
|
|
20
|
+
import { getUsdInrRate } from './brandsBilling.controller.js';
|
|
20
21
|
|
|
21
22
|
// Pulls CSM + Finance head emails (stored under applicationDefault
|
|
22
23
|
// type=invoice, subType=heads) AND the per-client CSMs assigned via
|
|
@@ -360,6 +361,103 @@ function getCurrentFinancialYear() {
|
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Shared annexure builder. Anchored to the invoice's BILLING month and
|
|
366
|
+
// mirroring invoice generation's billing-type rules: perZone / perCamera
|
|
367
|
+
// products multiply by the store's zone / camera count (see standardPrice).
|
|
368
|
+
// Returns the per-store rows plus the grand total shown at the end.
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
371
|
+
const billingMonth = invoiceInfo.billingDate ? dayjs( invoiceInfo.billingDate ) : dayjs();
|
|
372
|
+
const billingMonthEnd = new Date( billingMonth.endOf( 'month' ).toISOString() );
|
|
373
|
+
const monthDays = billingMonth.daysInMonth();
|
|
374
|
+
const invoiceCurrency = symbolFor( invoiceInfo.currency );
|
|
375
|
+
|
|
376
|
+
const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1 } );
|
|
377
|
+
const billingTypeMap = {};
|
|
378
|
+
( annexClient?.planDetails?.product || [] ).forEach( ( p ) => {
|
|
379
|
+
billingTypeMap[p.productName] = p.billingType || 'perStore';
|
|
380
|
+
} );
|
|
381
|
+
|
|
382
|
+
const rows = await dailyPricingService.aggregate( [
|
|
383
|
+
{ $match: { clientId: invoiceInfo.clientId, dateISO: { $lte: billingMonthEnd } } },
|
|
384
|
+
{ $sort: { dateISO: -1 } },
|
|
385
|
+
{ $limit: 1 },
|
|
386
|
+
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ?? [] ] } } } } },
|
|
387
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
388
|
+
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
389
|
+
{ $project: {
|
|
390
|
+
productName: '$stores.products.productName',
|
|
391
|
+
storeId: '$stores.storeId',
|
|
392
|
+
storeName: '$stores.storeName',
|
|
393
|
+
edgefirstFileDate: { $ifNull: [ '$stores.edgefirstFileDate', '$stores.processfirstFileDate' ] },
|
|
394
|
+
workingdays: '$stores.products.workingdays',
|
|
395
|
+
zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
|
|
396
|
+
trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
|
|
397
|
+
zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
|
|
398
|
+
} },
|
|
399
|
+
{ $match: { workingdays: { $gt: 0 } } },
|
|
400
|
+
{ $sort: { productName: 1, workingdays: -1 } },
|
|
401
|
+
{ $lookup: {
|
|
402
|
+
from: 'basepricings',
|
|
403
|
+
let: { clientId: invoiceInfo.clientId },
|
|
404
|
+
pipeline: [
|
|
405
|
+
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
406
|
+
{ $project: { standard: 1 } },
|
|
407
|
+
],
|
|
408
|
+
as: 'basepricing',
|
|
409
|
+
} },
|
|
410
|
+
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
411
|
+
{ $project: {
|
|
412
|
+
productName: 1, workingdays: 1, storeName: 1, storeId: 1, edgefirstFileDate: 1,
|
|
413
|
+
zoneCount: 1, trafficCameraCount: 1, zoneCameraCount: 1,
|
|
414
|
+
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
415
|
+
} },
|
|
416
|
+
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
417
|
+
] );
|
|
418
|
+
|
|
419
|
+
const data = rows.map( ( s ) => {
|
|
420
|
+
const billingType = billingTypeMap[s.productName] || 'perStore';
|
|
421
|
+
// Same units rule as invoice generation: perZone / perCamera multiply by
|
|
422
|
+
// the store's zone / camera count; anything else stays per-store.
|
|
423
|
+
let units = 1;
|
|
424
|
+
if ( s.productName === 'tangoZone' ) {
|
|
425
|
+
if ( billingType === 'perZone' && s.zoneCount > 0 ) {
|
|
426
|
+
units = s.zoneCount;
|
|
427
|
+
} else if ( billingType === 'perCamera' && s.zoneCameraCount > 0 ) {
|
|
428
|
+
units = s.zoneCameraCount;
|
|
429
|
+
}
|
|
430
|
+
} else if ( s.productName === 'tangoTraffic' ) {
|
|
431
|
+
if ( billingType === 'perCamera' && s.trafficCameraCount > 0 ) {
|
|
432
|
+
units = s.trafficCameraCount;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const price = Number( s.standard?.negotiatePrice ) || 0;
|
|
436
|
+
const runningCost = s.workingdays >= monthDays ?
|
|
437
|
+
Math.round( price * units * 100 ) / 100 :
|
|
438
|
+
Math.round( ( price / monthDays ) * s.workingdays * units * 100 ) / 100;
|
|
439
|
+
return {
|
|
440
|
+
productName: s.productName ? s.productName.charAt( 0 ).toUpperCase() + s.productName.slice( 1 ) : '',
|
|
441
|
+
currencyType: invoiceCurrency,
|
|
442
|
+
workingdays: s.workingdays,
|
|
443
|
+
storeName: s.storeName,
|
|
444
|
+
storeId: s.storeId,
|
|
445
|
+
edgefirstFileDate: s.edgefirstFileDate ? dayjs( s.edgefirstFileDate ).format( 'YYYY-MM-DD' ) : '',
|
|
446
|
+
period: s.workingdays < monthDays ? 'prorate' : 'fullmonth',
|
|
447
|
+
billingType,
|
|
448
|
+
// Human label for the unit-price column: what one unit means.
|
|
449
|
+
unitBasis: billingType === 'perCamera' ? 'Per Camera' : billingType === 'perZone' ? 'Per Zone' : 'Per Store',
|
|
450
|
+
units,
|
|
451
|
+
standardPrice: price,
|
|
452
|
+
runningCost,
|
|
453
|
+
};
|
|
454
|
+
} );
|
|
455
|
+
|
|
456
|
+
const totalAmount = Math.round( data.reduce( ( a, x ) => a + ( x.runningCost || 0 ), 0 ) * 100 ) / 100;
|
|
457
|
+
const totalFormatted = totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
458
|
+
return { data, totalAmount, totalFormatted };
|
|
459
|
+
}
|
|
460
|
+
|
|
363
461
|
export async function invoiceDownload( req, res ) {
|
|
364
462
|
try {
|
|
365
463
|
let invoiceData;
|
|
@@ -438,145 +536,10 @@ export async function invoiceDownload( req, res ) {
|
|
|
438
536
|
}
|
|
439
537
|
} );
|
|
440
538
|
}
|
|
441
|
-
const currentMonthDays = dayjs().daysInMonth();
|
|
442
|
-
|
|
443
539
|
if ( getgroup?.attachAnnexure ) {
|
|
444
|
-
|
|
445
|
-
{
|
|
446
|
-
$match: {
|
|
447
|
-
clientId: invoiceInfo.clientId,
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
$sort: { dateISO: -1 },
|
|
452
|
-
},
|
|
453
|
-
{ $limit: 1 },
|
|
454
|
-
{
|
|
455
|
-
$project: {
|
|
456
|
-
stores: {
|
|
457
|
-
$filter: {
|
|
458
|
-
input: '$stores',
|
|
459
|
-
as: 'item',
|
|
460
|
-
cond: { $in: [ '$$item.storeId', getgroup?.stores ] },
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
$unwind: {
|
|
467
|
-
path: '$stores',
|
|
468
|
-
preserveNullAndEmptyArrays: false,
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
$unwind: {
|
|
473
|
-
path: '$stores.products',
|
|
474
|
-
preserveNullAndEmptyArrays: false,
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
$project: {
|
|
479
|
-
productName: '$stores.products.productName',
|
|
480
|
-
storeId: '$stores.storeId',
|
|
481
|
-
storeName: '$stores.storeName',
|
|
482
|
-
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
483
|
-
workingdays: '$stores.products.workingdays',
|
|
484
|
-
currencyType: { $literal: invoiceCurrency },
|
|
485
|
-
},
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
$sort: {
|
|
489
|
-
productName: 1,
|
|
490
|
-
workingdays: -1,
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
$match: { workingdays: { $gt: 0 } },
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
{
|
|
498
|
-
$lookup: {
|
|
499
|
-
from: 'basepricings',
|
|
500
|
-
let: { clientId: invoiceInfo.clientId },
|
|
501
|
-
pipeline: [
|
|
502
|
-
{
|
|
503
|
-
$match: {
|
|
504
|
-
$expr: {
|
|
505
|
-
$eq: [ '$clientId', '$$clientId' ],
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
$project: {
|
|
511
|
-
standard: 1,
|
|
512
|
-
step: 1,
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
as: 'basepricing',
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
$unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true },
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
$project: {
|
|
524
|
-
productName: 1,
|
|
525
|
-
workingdays: 1,
|
|
526
|
-
storeName: 1,
|
|
527
|
-
currencyType: 1,
|
|
528
|
-
edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
|
|
529
|
-
storeId: 1,
|
|
530
|
-
standard: {
|
|
531
|
-
$filter: {
|
|
532
|
-
input: '$basepricing.standard',
|
|
533
|
-
as: 'standard',
|
|
534
|
-
cond: { $eq: [ '$$standard.productName', '$productName' ] },
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
step: '$basepricing.step',
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
$unwind: { path: '$standard', preserveNullAndEmptyArrays: true },
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
$project: {
|
|
545
|
-
productName: {
|
|
546
|
-
$concat: [
|
|
547
|
-
{ $toUpper: { $substr: [ '$productName', 0, 1 ] } }, // Uppercase first letter
|
|
548
|
-
{ $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] }, // Rest of the string
|
|
549
|
-
],
|
|
550
|
-
},
|
|
551
|
-
currencyType: 1,
|
|
552
|
-
workingdays: 1,
|
|
553
|
-
storeName: 1,
|
|
554
|
-
edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
|
|
555
|
-
storeId: 1,
|
|
556
|
-
period: {
|
|
557
|
-
$cond: {
|
|
558
|
-
if: { $lt: [ '$workingdays', currentMonthDays ] },
|
|
559
|
-
then: 'prorate',
|
|
560
|
-
else: 'fullmonth',
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
standardPrice: '$standard.negotiatePrice',
|
|
564
|
-
runningCost: {
|
|
565
|
-
$round: [
|
|
566
|
-
{
|
|
567
|
-
$multiply: [
|
|
568
|
-
{ $divide: [ '$standard.negotiatePrice', currentMonthDays ] },
|
|
569
|
-
'$workingdays',
|
|
570
|
-
],
|
|
571
|
-
},
|
|
572
|
-
2,
|
|
573
|
-
],
|
|
574
|
-
},
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
);
|
|
540
|
+
const { data: annuxureData, totalFormatted } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
579
541
|
invoiceData.annuxureData = annuxureData;
|
|
542
|
+
invoiceData.annuxureTotal = totalFormatted;
|
|
580
543
|
}
|
|
581
544
|
|
|
582
545
|
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
|
|
@@ -755,65 +718,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
755
718
|
} );
|
|
756
719
|
}
|
|
757
720
|
|
|
758
|
-
const currentMonthDays = dayjs().daysInMonth();
|
|
759
|
-
|
|
760
721
|
if ( getgroup?.attachAnnexure ) {
|
|
761
|
-
|
|
762
|
-
{ $match: { clientId: invoiceInfo.clientId } },
|
|
763
|
-
{ $sort: { dateISO: -1 } },
|
|
764
|
-
{ $limit: 1 },
|
|
765
|
-
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ] } } } } },
|
|
766
|
-
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
767
|
-
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
768
|
-
{ $project: {
|
|
769
|
-
productName: '$stores.products.productName',
|
|
770
|
-
storeId: '$stores.storeId',
|
|
771
|
-
storeName: '$stores.storeName',
|
|
772
|
-
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
773
|
-
workingdays: '$stores.products.workingdays',
|
|
774
|
-
currencyType: { $literal: invoiceCurrency },
|
|
775
|
-
} },
|
|
776
|
-
{ $sort: { productName: 1, workingdays: -1 } },
|
|
777
|
-
{ $match: { workingdays: { $gt: 0 } } },
|
|
778
|
-
{ $lookup: {
|
|
779
|
-
from: 'basepricings',
|
|
780
|
-
let: { clientId: invoiceInfo.clientId },
|
|
781
|
-
pipeline: [
|
|
782
|
-
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
783
|
-
{ $project: { standard: 1, step: 1 } },
|
|
784
|
-
],
|
|
785
|
-
as: 'basepricing',
|
|
786
|
-
} },
|
|
787
|
-
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
788
|
-
{ $project: {
|
|
789
|
-
productName: 1,
|
|
790
|
-
workingdays: 1,
|
|
791
|
-
storeName: 1,
|
|
792
|
-
currencyType: 1,
|
|
793
|
-
edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
|
|
794
|
-
storeId: 1,
|
|
795
|
-
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
796
|
-
step: '$basepricing.step',
|
|
797
|
-
} },
|
|
798
|
-
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
799
|
-
{ $project: {
|
|
800
|
-
productName: {
|
|
801
|
-
$concat: [
|
|
802
|
-
{ $toUpper: { $substr: [ '$productName', 0, 1 ] } },
|
|
803
|
-
{ $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] },
|
|
804
|
-
],
|
|
805
|
-
},
|
|
806
|
-
currencyType: 1,
|
|
807
|
-
workingdays: 1,
|
|
808
|
-
storeName: 1,
|
|
809
|
-
edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
|
|
810
|
-
storeId: 1,
|
|
811
|
-
period: { $cond: { if: { $lt: [ '$workingdays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
|
|
812
|
-
standardPrice: '$standard.negotiatePrice',
|
|
813
|
-
runningCost: { $round: [ { $multiply: [ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] }, '$workingdays' ] }, 2 ] },
|
|
814
|
-
} },
|
|
815
|
-
] );
|
|
722
|
+
const { data: annuxureData, totalFormatted } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
816
723
|
invoiceData.annuxureData = annuxureData;
|
|
724
|
+
invoiceData.annuxureTotal = totalFormatted;
|
|
817
725
|
}
|
|
818
726
|
|
|
819
727
|
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
|
|
@@ -926,68 +834,8 @@ export async function invoiceAnnexure( req, res ) {
|
|
|
926
834
|
return res.sendSuccess( { data: [] } );
|
|
927
835
|
}
|
|
928
836
|
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
// see invoiceDownload / buildInvoicePdfBuffer for the same pattern.
|
|
932
|
-
const invoiceCurrency = symbolFor( invoiceInfo.currency );
|
|
933
|
-
|
|
934
|
-
const annexureData = await dailyPricingService.aggregate( [
|
|
935
|
-
{ $match: { clientId: invoiceInfo.clientId } },
|
|
936
|
-
{ $sort: { dateISO: -1 } },
|
|
937
|
-
{ $limit: 1 },
|
|
938
|
-
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ?? [] ] } } } } },
|
|
939
|
-
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
940
|
-
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
941
|
-
{ $project: {
|
|
942
|
-
productName: '$stores.products.productName',
|
|
943
|
-
storeId: '$stores.storeId',
|
|
944
|
-
storeName: '$stores.storeName',
|
|
945
|
-
edgefirstFileDate: '$stores.edgefirstFileDate',
|
|
946
|
-
workingdays: '$stores.products.workingdays',
|
|
947
|
-
currencyType: { $literal: invoiceCurrency },
|
|
948
|
-
} },
|
|
949
|
-
{ $sort: { productName: 1, workingdays: -1 } },
|
|
950
|
-
{ $match: { workingdays: { $gt: 0 } } },
|
|
951
|
-
{ $lookup: {
|
|
952
|
-
from: 'basepricings',
|
|
953
|
-
let: { clientId: invoiceInfo.clientId },
|
|
954
|
-
pipeline: [
|
|
955
|
-
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
956
|
-
{ $project: { standard: 1, step: 1 } },
|
|
957
|
-
],
|
|
958
|
-
as: 'basepricing',
|
|
959
|
-
} },
|
|
960
|
-
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
961
|
-
{ $project: {
|
|
962
|
-
productName: 1,
|
|
963
|
-
workingdays: 1,
|
|
964
|
-
storeName: 1,
|
|
965
|
-
currencyType: 1,
|
|
966
|
-
edgefirstFileDate: { $ifNull: [ '$edgefirstFileDate', '$processfirstFileDate' ] },
|
|
967
|
-
storeId: 1,
|
|
968
|
-
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
969
|
-
step: '$basepricing.step',
|
|
970
|
-
} },
|
|
971
|
-
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
972
|
-
{ $project: {
|
|
973
|
-
productName: {
|
|
974
|
-
$concat: [
|
|
975
|
-
{ $toUpper: { $substr: [ '$productName', 0, 1 ] } },
|
|
976
|
-
{ $substr: [ '$productName', 1, { $strLenCP: '$productName' } ] },
|
|
977
|
-
],
|
|
978
|
-
},
|
|
979
|
-
currencyType: 1,
|
|
980
|
-
workingdays: 1,
|
|
981
|
-
storeName: 1,
|
|
982
|
-
edgefirstFileDate: { $dateToString: { format: '%Y-%m-%d', date: '$edgefirstFileDate' } },
|
|
983
|
-
storeId: 1,
|
|
984
|
-
period: { $cond: { if: { $lt: [ '$workingdays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
|
|
985
|
-
standardPrice: '$standard.negotiatePrice',
|
|
986
|
-
runningCost: { $round: [ { $multiply: [ { $divide: [ '$standard.negotiatePrice', currentMonthDays ] }, '$workingdays' ] }, 2 ] },
|
|
987
|
-
} },
|
|
988
|
-
] );
|
|
989
|
-
|
|
990
|
-
return res.sendSuccess( { data: annexureData } );
|
|
837
|
+
const { data, totalAmount } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
838
|
+
return res.sendSuccess( { data, totalAmount } );
|
|
991
839
|
} catch ( error ) {
|
|
992
840
|
logger.error( { error: error, function: 'invoiceAnnexure', invoiceId: req.params.invoiceId } );
|
|
993
841
|
return res.sendError( error, 500 );
|
|
@@ -1857,7 +1705,19 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1857
1705
|
filterStartDate = new Date( dayjs().subtract( 1, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1858
1706
|
filterEndDate = new Date( dayjs().subtract( 1, 'month' ).endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1859
1707
|
}
|
|
1860
|
-
|
|
1708
|
+
// Rolling windows. 'last3' = the prototype's "Last 3 Months" (was the
|
|
1709
|
+
// legacy 'last' id, which silently meant 12 months and made every other
|
|
1710
|
+
// filter look broken). 'last' is kept as an alias of 'last3' so older
|
|
1711
|
+
// clients don't break mid-deploy.
|
|
1712
|
+
if ( req.body?.filter && ( req.body.filter == 'last3' || req.body.filter == 'last' ) ) {
|
|
1713
|
+
filterStartDate = new Date( dayjs().subtract( 3, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1714
|
+
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1715
|
+
}
|
|
1716
|
+
if ( req.body?.filter && req.body.filter == 'last6' ) {
|
|
1717
|
+
filterStartDate = new Date( dayjs().subtract( 6, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1718
|
+
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1719
|
+
}
|
|
1720
|
+
if ( req.body?.filter && req.body.filter == 'last12' ) {
|
|
1861
1721
|
filterStartDate = new Date( dayjs().subtract( 12, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1862
1722
|
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1863
1723
|
}
|
|
@@ -1943,6 +1803,14 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1943
1803
|
paidAmount: { $ifNull: [ '$paidAmount', 0 ] },
|
|
1944
1804
|
clientId: 1,
|
|
1945
1805
|
billingDate: 1,
|
|
1806
|
+
companyName: 1,
|
|
1807
|
+
dueDate: 1,
|
|
1808
|
+
// GST column shown in the table. Derived rather than stored because
|
|
1809
|
+
// dollar-currency invoices have totalAmount === amount (no GST).
|
|
1810
|
+
gstAmount: { $subtract: [
|
|
1811
|
+
{ $ifNull: [ '$totalAmount', 0 ] },
|
|
1812
|
+
{ $ifNull: [ '$amount', 0 ] },
|
|
1813
|
+
] },
|
|
1946
1814
|
},
|
|
1947
1815
|
},
|
|
1948
1816
|
|
|
@@ -1953,6 +1821,8 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1953
1821
|
$or: [
|
|
1954
1822
|
{ groupName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1955
1823
|
{ clientName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1824
|
+
{ companyName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1825
|
+
{ invoice: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1956
1826
|
{ paymentStatus: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1957
1827
|
{ status: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1958
1828
|
],
|
|
@@ -1987,11 +1857,15 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1987
1857
|
const exportdata = [];
|
|
1988
1858
|
count.forEach( ( element ) => {
|
|
1989
1859
|
exportdata.push( {
|
|
1990
|
-
'
|
|
1860
|
+
'Brand Name': element.clientName,
|
|
1861
|
+
'Registered Name': element.companyName || '',
|
|
1991
1862
|
'Invoice #': element.invoice,
|
|
1992
1863
|
'Billing date': dayjs( element.billingDate ).format( 'DD MMM, YYYY' ),
|
|
1864
|
+
'Due Date': element.dueDate ? dayjs( element.dueDate ).format( 'DD MMM, YYYY' ) : '',
|
|
1993
1865
|
'Group Name': element.groupName,
|
|
1994
|
-
'Amount': element.
|
|
1866
|
+
'Amount Excl. GST': element.amount,
|
|
1867
|
+
'GST Amount': element.gstAmount,
|
|
1868
|
+
'Amount Incl. GST': element.totalAmount,
|
|
1995
1869
|
'Stores': element.stores,
|
|
1996
1870
|
'Payment Status': element.paymentStatus,
|
|
1997
1871
|
'Approval Status': element.status,
|
|
@@ -2021,7 +1895,45 @@ export async function clientInvoiceList( req, res ) {
|
|
|
2021
1895
|
client.logo = '';
|
|
2022
1896
|
}
|
|
2023
1897
|
}
|
|
2024
|
-
|
|
1898
|
+
|
|
1899
|
+
// Card totals — computed over the CURRENTLY-FILTERED result set (`count`
|
|
1900
|
+
// holds every invoice matching the active filters, pre-pagination), so the
|
|
1901
|
+
// cards reflect exactly what the filters select. Dollar invoices are
|
|
1902
|
+
// converted to INR at today's rate so all three totals are a single ₹
|
|
1903
|
+
// figure. Outstanding = unpaid remaining (totalAmount - paidAmount);
|
|
1904
|
+
// Overdue = past-due unpaid subset; Pending Payment = approved-but-unpaid.
|
|
1905
|
+
const usdRate = await getUsdInrRate();
|
|
1906
|
+
const now = new Date();
|
|
1907
|
+
const cards = {
|
|
1908
|
+
outstandingAmount: 0, outstandingCount: 0,
|
|
1909
|
+
overdueAmount: 0, overdueCount: 0,
|
|
1910
|
+
pendingPaymentAmount: 0, pendingPaymentCount: 0,
|
|
1911
|
+
};
|
|
1912
|
+
for ( const inv of count ) {
|
|
1913
|
+
if ( inv.paymentStatus === 'paid' ) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
const fx = inv.currency === 'dollar' ? usdRate : 1;
|
|
1917
|
+
const total = Number( inv.totalAmount ) || Number( inv.amount ) || 0;
|
|
1918
|
+
const paid = Number( inv.paidAmount ) || 0;
|
|
1919
|
+
const remaining = Math.max( 0, total - paid ) * fx;
|
|
1920
|
+
|
|
1921
|
+
cards.outstandingAmount += remaining;
|
|
1922
|
+
cards.outstandingCount += 1;
|
|
1923
|
+
if ( inv.dueDate && new Date( inv.dueDate ) < now ) {
|
|
1924
|
+
cards.overdueAmount += remaining;
|
|
1925
|
+
cards.overdueCount += 1;
|
|
1926
|
+
}
|
|
1927
|
+
if ( inv.status === 'approved' ) {
|
|
1928
|
+
cards.pendingPaymentAmount += remaining;
|
|
1929
|
+
cards.pendingPaymentCount += 1;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
cards.outstandingAmount = Math.round( cards.outstandingAmount * 100 ) / 100;
|
|
1933
|
+
cards.overdueAmount = Math.round( cards.overdueAmount * 100 ) / 100;
|
|
1934
|
+
cards.pendingPaymentAmount = Math.round( cards.pendingPaymentAmount * 100 ) / 100;
|
|
1935
|
+
|
|
1936
|
+
res.sendSuccess( { count: count.length, data: invoiceList, cards } );
|
|
2025
1937
|
} catch ( error ) {
|
|
2026
1938
|
logger.error( { error: error, function: 'clientInvoiceList' } );
|
|
2027
1939
|
return res.sendError( error, 500 );
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as paymentReminderService from '../services/paymentReminder.service.js';
|
|
2
|
+
import { logger } from 'tango-app-api-middleware';
|
|
3
|
+
|
|
4
|
+
// Payment reminder config (Billing Settings page). One document per brand
|
|
5
|
+
// (clientId): recipient emails + five toggleable reminder templates. Returns
|
|
6
|
+
// sensible defaults when a brand has no config saved yet.
|
|
7
|
+
const DEFAULTS = () => ( {
|
|
8
|
+
reminderEmails: [],
|
|
9
|
+
templates: {
|
|
10
|
+
preDue: { enabled: true, daysBefore: 3 },
|
|
11
|
+
onDue: { enabled: true },
|
|
12
|
+
onHold: { enabled: true },
|
|
13
|
+
suspend: { enabled: true },
|
|
14
|
+
deactivated: { enabled: false },
|
|
15
|
+
},
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
export async function getPaymentReminder( req, res ) {
|
|
19
|
+
try {
|
|
20
|
+
const clientId = req.params.clientId || req.query.clientId;
|
|
21
|
+
if ( !clientId ) {
|
|
22
|
+
return res.sendError( 'clientId is required', 400 );
|
|
23
|
+
}
|
|
24
|
+
const existing = await paymentReminderService.findOne( { clientId } );
|
|
25
|
+
if ( !existing ) {
|
|
26
|
+
return res.sendSuccess( { clientId, ...DEFAULTS(), isDefault: true } );
|
|
27
|
+
}
|
|
28
|
+
return res.sendSuccess( existing );
|
|
29
|
+
} catch ( error ) {
|
|
30
|
+
logger.error( { error: error, function: 'getPaymentReminder' } );
|
|
31
|
+
return res.sendError( error, 500 );
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function savePaymentReminder( req, res ) {
|
|
36
|
+
try {
|
|
37
|
+
const b = req.body || {};
|
|
38
|
+
if ( !b.clientId ) {
|
|
39
|
+
return res.sendError( 'clientId is required', 400 );
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Normalize recipients: trim, drop blanks, de-dupe.
|
|
43
|
+
const emails = Array.isArray( b.reminderEmails ) ? b.reminderEmails : [];
|
|
44
|
+
const reminderEmails = [ ...new Set(
|
|
45
|
+
emails.map( ( e ) => String( e || '' ).trim() ).filter( Boolean ),
|
|
46
|
+
) ];
|
|
47
|
+
|
|
48
|
+
const t = b.templates || {};
|
|
49
|
+
const bool = ( v, d ) => ( typeof v === 'boolean' ? v : d );
|
|
50
|
+
let daysBefore = Number( t.preDue?.daysBefore );
|
|
51
|
+
if ( !Number.isFinite( daysBefore ) ) {
|
|
52
|
+
daysBefore = 3;
|
|
53
|
+
}
|
|
54
|
+
daysBefore = Math.min( 365, Math.max( 1, Math.round( daysBefore ) ) );
|
|
55
|
+
|
|
56
|
+
const templates = {
|
|
57
|
+
preDue: { enabled: bool( t.preDue?.enabled, true ), daysBefore },
|
|
58
|
+
onDue: { enabled: bool( t.onDue?.enabled, true ) },
|
|
59
|
+
onHold: { enabled: bool( t.onHold?.enabled, true ) },
|
|
60
|
+
suspend: { enabled: bool( t.suspend?.enabled, true ) },
|
|
61
|
+
deactivated: { enabled: bool( t.deactivated?.enabled, false ) },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await paymentReminderService.upsert(
|
|
65
|
+
{ clientId: b.clientId },
|
|
66
|
+
{
|
|
67
|
+
clientId: b.clientId,
|
|
68
|
+
reminderEmails,
|
|
69
|
+
templates,
|
|
70
|
+
updatedBy: req.user?.email || req.user?.userName || '',
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const saved = await paymentReminderService.findOne( { clientId: b.clientId } );
|
|
75
|
+
logger.info?.( { function: 'savePaymentReminder', clientId: b.clientId } );
|
|
76
|
+
return res.sendSuccess( saved );
|
|
77
|
+
} catch ( error ) {
|
|
78
|
+
logger.error( { error: error, function: 'savePaymentReminder' } );
|
|
79
|
+
return res.sendError( error, 500 );
|
|
80
|
+
}
|
|
81
|
+
}
|