tango-app-api-payment-subscription 3.5.5 → 3.5.6
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 +289 -0
- package/src/controllers/estimate.controller.js +317 -0
- package/src/controllers/invoice.controller.js +164 -260
- 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/brandsBilling.routes.js +2 -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
|
@@ -360,6 +360,103 @@ function getCurrentFinancialYear() {
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Shared annexure builder. Anchored to the invoice's BILLING month and
|
|
365
|
+
// mirroring invoice generation's billing-type rules: perZone / perCamera
|
|
366
|
+
// products multiply by the store's zone / camera count (see standardPrice).
|
|
367
|
+
// Returns the per-store rows plus the grand total shown at the end.
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
async function buildAnnexureRows( invoiceInfo, getgroup ) {
|
|
370
|
+
const billingMonth = invoiceInfo.billingDate ? dayjs( invoiceInfo.billingDate ) : dayjs();
|
|
371
|
+
const billingMonthEnd = new Date( billingMonth.endOf( 'month' ).toISOString() );
|
|
372
|
+
const monthDays = billingMonth.daysInMonth();
|
|
373
|
+
const invoiceCurrency = symbolFor( invoiceInfo.currency );
|
|
374
|
+
|
|
375
|
+
const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1 } );
|
|
376
|
+
const billingTypeMap = {};
|
|
377
|
+
( annexClient?.planDetails?.product || [] ).forEach( ( p ) => {
|
|
378
|
+
billingTypeMap[p.productName] = p.billingType || 'perStore';
|
|
379
|
+
} );
|
|
380
|
+
|
|
381
|
+
const rows = await dailyPricingService.aggregate( [
|
|
382
|
+
{ $match: { clientId: invoiceInfo.clientId, dateISO: { $lte: billingMonthEnd } } },
|
|
383
|
+
{ $sort: { dateISO: -1 } },
|
|
384
|
+
{ $limit: 1 },
|
|
385
|
+
{ $project: { stores: { $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', getgroup?.stores ?? [] ] } } } } },
|
|
386
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
387
|
+
{ $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
|
|
388
|
+
{ $project: {
|
|
389
|
+
productName: '$stores.products.productName',
|
|
390
|
+
storeId: '$stores.storeId',
|
|
391
|
+
storeName: '$stores.storeName',
|
|
392
|
+
edgefirstFileDate: { $ifNull: [ '$stores.edgefirstFileDate', '$stores.processfirstFileDate' ] },
|
|
393
|
+
workingdays: '$stores.products.workingdays',
|
|
394
|
+
zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
|
|
395
|
+
trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
|
|
396
|
+
zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
|
|
397
|
+
} },
|
|
398
|
+
{ $match: { workingdays: { $gt: 0 } } },
|
|
399
|
+
{ $sort: { productName: 1, workingdays: -1 } },
|
|
400
|
+
{ $lookup: {
|
|
401
|
+
from: 'basepricings',
|
|
402
|
+
let: { clientId: invoiceInfo.clientId },
|
|
403
|
+
pipeline: [
|
|
404
|
+
{ $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
|
|
405
|
+
{ $project: { standard: 1 } },
|
|
406
|
+
],
|
|
407
|
+
as: 'basepricing',
|
|
408
|
+
} },
|
|
409
|
+
{ $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
|
|
410
|
+
{ $project: {
|
|
411
|
+
productName: 1, workingdays: 1, storeName: 1, storeId: 1, edgefirstFileDate: 1,
|
|
412
|
+
zoneCount: 1, trafficCameraCount: 1, zoneCameraCount: 1,
|
|
413
|
+
standard: { $filter: { input: '$basepricing.standard', as: 'standard', cond: { $eq: [ '$$standard.productName', '$productName' ] } } },
|
|
414
|
+
} },
|
|
415
|
+
{ $unwind: { path: '$standard', preserveNullAndEmptyArrays: true } },
|
|
416
|
+
] );
|
|
417
|
+
|
|
418
|
+
const data = rows.map( ( s ) => {
|
|
419
|
+
const billingType = billingTypeMap[s.productName] || 'perStore';
|
|
420
|
+
// Same units rule as invoice generation: perZone / perCamera multiply by
|
|
421
|
+
// the store's zone / camera count; anything else stays per-store.
|
|
422
|
+
let units = 1;
|
|
423
|
+
if ( s.productName === 'tangoZone' ) {
|
|
424
|
+
if ( billingType === 'perZone' && s.zoneCount > 0 ) {
|
|
425
|
+
units = s.zoneCount;
|
|
426
|
+
} else if ( billingType === 'perCamera' && s.zoneCameraCount > 0 ) {
|
|
427
|
+
units = s.zoneCameraCount;
|
|
428
|
+
}
|
|
429
|
+
} else if ( s.productName === 'tangoTraffic' ) {
|
|
430
|
+
if ( billingType === 'perCamera' && s.trafficCameraCount > 0 ) {
|
|
431
|
+
units = s.trafficCameraCount;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const price = Number( s.standard?.negotiatePrice ) || 0;
|
|
435
|
+
const runningCost = s.workingdays >= monthDays ?
|
|
436
|
+
Math.round( price * units * 100 ) / 100 :
|
|
437
|
+
Math.round( ( price / monthDays ) * s.workingdays * units * 100 ) / 100;
|
|
438
|
+
return {
|
|
439
|
+
productName: s.productName ? s.productName.charAt( 0 ).toUpperCase() + s.productName.slice( 1 ) : '',
|
|
440
|
+
currencyType: invoiceCurrency,
|
|
441
|
+
workingdays: s.workingdays,
|
|
442
|
+
storeName: s.storeName,
|
|
443
|
+
storeId: s.storeId,
|
|
444
|
+
edgefirstFileDate: s.edgefirstFileDate ? dayjs( s.edgefirstFileDate ).format( 'YYYY-MM-DD' ) : '',
|
|
445
|
+
period: s.workingdays < monthDays ? 'prorate' : 'fullmonth',
|
|
446
|
+
billingType,
|
|
447
|
+
// Human label for the unit-price column: what one unit means.
|
|
448
|
+
unitBasis: billingType === 'perCamera' ? 'Per Camera' : billingType === 'perZone' ? 'Per Zone' : 'Per Store',
|
|
449
|
+
units,
|
|
450
|
+
standardPrice: price,
|
|
451
|
+
runningCost,
|
|
452
|
+
};
|
|
453
|
+
} );
|
|
454
|
+
|
|
455
|
+
const totalAmount = Math.round( data.reduce( ( a, x ) => a + ( x.runningCost || 0 ), 0 ) * 100 ) / 100;
|
|
456
|
+
const totalFormatted = totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
457
|
+
return { data, totalAmount, totalFormatted };
|
|
458
|
+
}
|
|
459
|
+
|
|
363
460
|
export async function invoiceDownload( req, res ) {
|
|
364
461
|
try {
|
|
365
462
|
let invoiceData;
|
|
@@ -438,145 +535,10 @@ export async function invoiceDownload( req, res ) {
|
|
|
438
535
|
}
|
|
439
536
|
} );
|
|
440
537
|
}
|
|
441
|
-
const currentMonthDays = dayjs().daysInMonth();
|
|
442
|
-
|
|
443
538
|
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
|
-
);
|
|
539
|
+
const { data: annuxureData, totalFormatted } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
579
540
|
invoiceData.annuxureData = annuxureData;
|
|
541
|
+
invoiceData.annuxureTotal = totalFormatted;
|
|
580
542
|
}
|
|
581
543
|
|
|
582
544
|
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
|
|
@@ -755,65 +717,10 @@ async function buildInvoicePdfBuffer( invoiceId ) {
|
|
|
755
717
|
} );
|
|
756
718
|
}
|
|
757
719
|
|
|
758
|
-
const currentMonthDays = dayjs().daysInMonth();
|
|
759
|
-
|
|
760
720
|
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
|
-
] );
|
|
721
|
+
const { data: annuxureData, totalFormatted } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
816
722
|
invoiceData.annuxureData = annuxureData;
|
|
723
|
+
invoiceData.annuxureTotal = totalFormatted;
|
|
817
724
|
}
|
|
818
725
|
|
|
819
726
|
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/invoicePdf.hbs', 'utf8' );
|
|
@@ -926,68 +833,8 @@ export async function invoiceAnnexure( req, res ) {
|
|
|
926
833
|
return res.sendSuccess( { data: [] } );
|
|
927
834
|
}
|
|
928
835
|
|
|
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 } );
|
|
836
|
+
const { data, totalAmount } = await buildAnnexureRows( invoiceInfo, getgroup );
|
|
837
|
+
return res.sendSuccess( { data, totalAmount } );
|
|
991
838
|
} catch ( error ) {
|
|
992
839
|
logger.error( { error: error, function: 'invoiceAnnexure', invoiceId: req.params.invoiceId } );
|
|
993
840
|
return res.sendError( error, 500 );
|
|
@@ -1857,7 +1704,19 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1857
1704
|
filterStartDate = new Date( dayjs().subtract( 1, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1858
1705
|
filterEndDate = new Date( dayjs().subtract( 1, 'month' ).endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1859
1706
|
}
|
|
1860
|
-
|
|
1707
|
+
// Rolling windows. 'last3' = the prototype's "Last 3 Months" (was the
|
|
1708
|
+
// legacy 'last' id, which silently meant 12 months and made every other
|
|
1709
|
+
// filter look broken). 'last' is kept as an alias of 'last3' so older
|
|
1710
|
+
// clients don't break mid-deploy.
|
|
1711
|
+
if ( req.body?.filter && ( req.body.filter == 'last3' || req.body.filter == 'last' ) ) {
|
|
1712
|
+
filterStartDate = new Date( dayjs().subtract( 3, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1713
|
+
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1714
|
+
}
|
|
1715
|
+
if ( req.body?.filter && req.body.filter == 'last6' ) {
|
|
1716
|
+
filterStartDate = new Date( dayjs().subtract( 6, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1717
|
+
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1718
|
+
}
|
|
1719
|
+
if ( req.body?.filter && req.body.filter == 'last12' ) {
|
|
1861
1720
|
filterStartDate = new Date( dayjs().subtract( 12, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1862
1721
|
filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
|
|
1863
1722
|
}
|
|
@@ -1943,6 +1802,14 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1943
1802
|
paidAmount: { $ifNull: [ '$paidAmount', 0 ] },
|
|
1944
1803
|
clientId: 1,
|
|
1945
1804
|
billingDate: 1,
|
|
1805
|
+
companyName: 1,
|
|
1806
|
+
dueDate: 1,
|
|
1807
|
+
// GST column shown in the table. Derived rather than stored because
|
|
1808
|
+
// dollar-currency invoices have totalAmount === amount (no GST).
|
|
1809
|
+
gstAmount: { $subtract: [
|
|
1810
|
+
{ $ifNull: [ '$totalAmount', 0 ] },
|
|
1811
|
+
{ $ifNull: [ '$amount', 0 ] },
|
|
1812
|
+
] },
|
|
1946
1813
|
},
|
|
1947
1814
|
},
|
|
1948
1815
|
|
|
@@ -1953,6 +1820,8 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1953
1820
|
$or: [
|
|
1954
1821
|
{ groupName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1955
1822
|
{ clientName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1823
|
+
{ companyName: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1824
|
+
{ invoice: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1956
1825
|
{ paymentStatus: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1957
1826
|
{ status: { $regex: req.body.searchValue, $options: 'i' } },
|
|
1958
1827
|
],
|
|
@@ -1987,11 +1856,15 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1987
1856
|
const exportdata = [];
|
|
1988
1857
|
count.forEach( ( element ) => {
|
|
1989
1858
|
exportdata.push( {
|
|
1990
|
-
'
|
|
1859
|
+
'Brand Name': element.clientName,
|
|
1860
|
+
'Registered Name': element.companyName || '',
|
|
1991
1861
|
'Invoice #': element.invoice,
|
|
1992
1862
|
'Billing date': dayjs( element.billingDate ).format( 'DD MMM, YYYY' ),
|
|
1863
|
+
'Due Date': element.dueDate ? dayjs( element.dueDate ).format( 'DD MMM, YYYY' ) : '',
|
|
1993
1864
|
'Group Name': element.groupName,
|
|
1994
|
-
'Amount': element.
|
|
1865
|
+
'Amount Excl. GST': element.amount,
|
|
1866
|
+
'GST Amount': element.gstAmount,
|
|
1867
|
+
'Amount Incl. GST': element.totalAmount,
|
|
1995
1868
|
'Stores': element.stores,
|
|
1996
1869
|
'Payment Status': element.paymentStatus,
|
|
1997
1870
|
'Approval Status': element.status,
|
|
@@ -2021,7 +1894,38 @@ export async function clientInvoiceList( req, res ) {
|
|
|
2021
1894
|
client.logo = '';
|
|
2022
1895
|
}
|
|
2023
1896
|
}
|
|
2024
|
-
|
|
1897
|
+
|
|
1898
|
+
// Card totals — computed over the user's full client scope (findClients),
|
|
1899
|
+
// NOT the currently-filtered/paged view, so the cards stay stable as the
|
|
1900
|
+
// user narrows the list with filters. Outstanding is everything unpaid;
|
|
1901
|
+
// Overdue is the past-due subset; Pending Payment is the approved-but-
|
|
1902
|
+
// unpaid subset. "Outstanding amount" uses totalAmount - paidAmount so a
|
|
1903
|
+
// partially-paid invoice contributes only its remaining balance.
|
|
1904
|
+
const now = new Date();
|
|
1905
|
+
const remaining = { $subtract: [
|
|
1906
|
+
{ $ifNull: [ '$totalAmount', { $ifNull: [ '$amount', 0 ] } ] },
|
|
1907
|
+
{ $ifNull: [ '$paidAmount', 0 ] },
|
|
1908
|
+
] };
|
|
1909
|
+
const cardsAggregate = await invoiceService.aggregate( [
|
|
1910
|
+
{ $match: { clientId: { $in: findClients }, paymentStatus: { $ne: 'paid' } } },
|
|
1911
|
+
{ $group: {
|
|
1912
|
+
_id: null,
|
|
1913
|
+
outstandingAmount: { $sum: remaining },
|
|
1914
|
+
outstandingCount: { $sum: 1 },
|
|
1915
|
+
overdueAmount: { $sum: { $cond: [ { $lt: [ '$dueDate', now ] }, remaining, 0 ] } },
|
|
1916
|
+
overdueCount: { $sum: { $cond: [ { $lt: [ '$dueDate', now ] }, 1, 0 ] } },
|
|
1917
|
+
pendingPaymentAmount: { $sum: { $cond: [ { $eq: [ '$status', 'approved' ] }, remaining, 0 ] } },
|
|
1918
|
+
pendingPaymentCount: { $sum: { $cond: [ { $eq: [ '$status', 'approved' ] }, 1, 0 ] } },
|
|
1919
|
+
} },
|
|
1920
|
+
] );
|
|
1921
|
+
const cards = cardsAggregate[0] || {
|
|
1922
|
+
outstandingAmount: 0, outstandingCount: 0,
|
|
1923
|
+
overdueAmount: 0, overdueCount: 0,
|
|
1924
|
+
pendingPaymentAmount: 0, pendingPaymentCount: 0,
|
|
1925
|
+
};
|
|
1926
|
+
delete cards._id;
|
|
1927
|
+
|
|
1928
|
+
res.sendSuccess( { count: count.length, data: invoiceList, cards } );
|
|
2025
1929
|
} catch ( error ) {
|
|
2026
1930
|
logger.error( { error: error, function: 'clientInvoiceList' } );
|
|
2027
1931
|
return res.sendError( error, 500 );
|
|
@@ -102,6 +102,15 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
|
|
|
102
102
|
let storeCount = await storeService.count( { clientId: clientInfo[0].clientId, status: 'active' } );
|
|
103
103
|
let tangoProductsList = await basePricingService.findOne( { clientId: { $exists: false } }, { basePricing: 1 } );
|
|
104
104
|
let tangoProducts = tangoProductsList.basePricing.map( ( item ) => item.productName );
|
|
105
|
+
// Client-specific negotiated pricing — used to show the agreed per-store
|
|
106
|
+
// price for subscribed (live) products in the upgrade-plan popup.
|
|
107
|
+
let clientPricing = await basePricingService.findOne( { clientId: clientInfo[0].clientId }, { standard: 1, step: 1 } );
|
|
108
|
+
let negotiateByProduct = {};
|
|
109
|
+
( clientPricing?.standard || clientPricing?.step || [] ).forEach( ( p ) => {
|
|
110
|
+
if ( p && p.productName != null && p.negotiatePrice != null ) {
|
|
111
|
+
negotiateByProduct[p.productName] = p.negotiatePrice;
|
|
112
|
+
}
|
|
113
|
+
} );
|
|
105
114
|
let activeProducts = clientInfo[0].planDetails.product;
|
|
106
115
|
let liveProducts = [];
|
|
107
116
|
let trialProducts = [];
|
|
@@ -160,6 +169,10 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
|
|
|
160
169
|
if ( price ) {
|
|
161
170
|
element.price = price.basePrice;
|
|
162
171
|
}
|
|
172
|
+
// Negotiated (agreed) per-store price for subscribed products.
|
|
173
|
+
if ( negotiateByProduct[element.productName] != null ) {
|
|
174
|
+
element.negotiatePrice = negotiateByProduct[element.productName];
|
|
175
|
+
}
|
|
163
176
|
let getProductCount = productDetails.find( ( item ) => item.product == element.productName );
|
|
164
177
|
element.storeCount = getProductCount?.count || 0;
|
|
165
178
|
element.aliseProductName = convertTitleCase( element.productName );
|
|
@@ -2546,17 +2559,55 @@ async function updatePricing( req, res, update ) {
|
|
|
2546
2559
|
if ( clientDetails ) {
|
|
2547
2560
|
let products = clientDetails.planDetails.product.map( ( item ) => item.productName );
|
|
2548
2561
|
let subscriptionProduct = clientDetails.planDetails.product.filter( ( item ) => item.status == 'live' );
|
|
2562
|
+
// Negotiated prices coming from the Subscribe popup (or any caller) take
|
|
2563
|
+
// precedence; otherwise we keep whatever was already saved for this
|
|
2564
|
+
// client and only fall back to the global base defaults for brand-new
|
|
2565
|
+
// products. This stops updatePricing from clobbering a manually agreed
|
|
2566
|
+
// price every time the product list changes.
|
|
2567
|
+
let pricingOverride = {};
|
|
2568
|
+
if ( Array.isArray( req.body.pricing ) ) {
|
|
2569
|
+
req.body.pricing.forEach( ( p ) => {
|
|
2570
|
+
if ( p && p.productName ) {
|
|
2571
|
+
pricingOverride[p.productName] = p;
|
|
2572
|
+
}
|
|
2573
|
+
} );
|
|
2574
|
+
}
|
|
2575
|
+
logger.info?.( { function: 'updatePricing', clientId: req.body.clientId, override: pricingOverride } );
|
|
2576
|
+
let existingStandard = {};
|
|
2577
|
+
let existingStep = {};
|
|
2578
|
+
( getPriceInfo?.standard || [] ).forEach( ( s ) => {
|
|
2579
|
+
existingStandard[s.productName] = s;
|
|
2580
|
+
} );
|
|
2581
|
+
( getPriceInfo?.step || [] ).forEach( ( s ) => {
|
|
2582
|
+
existingStep[s.productName] = s;
|
|
2583
|
+
} );
|
|
2549
2584
|
let standardList = [];
|
|
2550
2585
|
let stepList = [];
|
|
2551
2586
|
products.forEach( ( product ) => {
|
|
2552
2587
|
let baseDetails = baseProduct.basePricing.find( ( item ) => item.productName == product );
|
|
2588
|
+
// A product missing from the global base pricing would otherwise crash
|
|
2589
|
+
// the whole pricing save (and silently drop the negotiated override,
|
|
2590
|
+
// since this runs un-awaited in a .then()). Fall back to safe zeros.
|
|
2591
|
+
if ( !baseDetails ) {
|
|
2592
|
+
baseDetails = { basePrice: 0, discoutPercentage: 0 };
|
|
2593
|
+
}
|
|
2553
2594
|
let discountPrice = ( baseDetails.basePrice * baseDetails.discoutPercentage ) / 100;
|
|
2595
|
+
let defaultNegotiate = Number( baseDetails.basePrice - discountPrice );
|
|
2596
|
+
// Precedence: explicit override from the request > previously saved
|
|
2597
|
+
// client price > computed default from the global base pricing.
|
|
2598
|
+
let override = pricingOverride[product];
|
|
2599
|
+
let prevStandard = existingStandard[product];
|
|
2600
|
+
let prevStep = existingStep[product];
|
|
2601
|
+
let negotiateStandard = override && override.negotiatePrice != null ? Number( override.negotiatePrice ) :
|
|
2602
|
+
( prevStandard && prevStandard.negotiatePrice != null ? Number( prevStandard.negotiatePrice ) : defaultNegotiate );
|
|
2603
|
+
let negotiateStep = override && override.negotiatePrice != null ? Number( override.negotiatePrice ) :
|
|
2604
|
+
( prevStep && prevStep.negotiatePrice != null ? Number( prevStep.negotiatePrice ) : defaultNegotiate );
|
|
2554
2605
|
standardList.push(
|
|
2555
2606
|
{
|
|
2556
2607
|
productName: product,
|
|
2557
2608
|
discountPercentage: baseDetails.discoutPercentage,
|
|
2558
2609
|
basePrice: baseDetails.basePrice,
|
|
2559
|
-
negotiatePrice:
|
|
2610
|
+
negotiatePrice: negotiateStandard,
|
|
2560
2611
|
},
|
|
2561
2612
|
);
|
|
2562
2613
|
stepList.push(
|
|
@@ -2564,8 +2615,8 @@ async function updatePricing( req, res, update ) {
|
|
|
2564
2615
|
productName: product,
|
|
2565
2616
|
discountPercentage: baseDetails.discoutPercentage,
|
|
2566
2617
|
basePrice: baseDetails.basePrice,
|
|
2567
|
-
negotiatePrice:
|
|
2568
|
-
storeRange: '1-100',
|
|
2618
|
+
negotiatePrice: negotiateStep,
|
|
2619
|
+
storeRange: ( prevStep && prevStep.storeRange ) || '1-100',
|
|
2569
2620
|
},
|
|
2570
2621
|
);
|
|
2571
2622
|
} );
|
|
@@ -2574,6 +2625,7 @@ async function updatePricing( req, res, update ) {
|
|
|
2574
2625
|
step: stepList,
|
|
2575
2626
|
clientId: req.body.clientId,
|
|
2576
2627
|
};
|
|
2628
|
+
console.log( '🚀 ~ updatePricing ~ data:', data );
|
|
2577
2629
|
if ( !getPriceInfo ) {
|
|
2578
2630
|
await basePricingService.create( data );
|
|
2579
2631
|
} else {
|
|
@@ -75,6 +75,12 @@ export const validateSubscibeSchema = joi.object( {
|
|
|
75
75
|
product: joi.array().required(),
|
|
76
76
|
clientId: joi.string().required(),
|
|
77
77
|
stores: joi.array().optional(),
|
|
78
|
+
// Negotiated price overrides from the Subscribe popup — applied in
|
|
79
|
+
// updatePricing so a manually-agreed price isn't reset to base defaults.
|
|
80
|
+
pricing: joi.array().items( joi.object( {
|
|
81
|
+
productName: joi.string().required(),
|
|
82
|
+
negotiatePrice: joi.number().min( 0 ).required(),
|
|
83
|
+
} ).unknown( true ) ).optional(),
|
|
78
84
|
} );
|
|
79
85
|
|
|
80
86
|
export const validateSubscibeParams = {
|