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.
@@ -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
- let annuxureData = await dailyPricingService.aggregate( [
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
- let annuxureData = await dailyPricingService.aggregate( [
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 currentMonthDays = dayjs().daysInMonth();
930
- // Annexure must show the same currency as the invoice it accompanies —
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
- if ( req.body?.filter && req.body.filter == 'last' ) {
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
- 'Client Name': element.clientName,
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.totalAmount,
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
- res.sendSuccess( { count: count.length, data: invoiceList } );
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: Number( baseDetails.basePrice - discountPrice ),
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: Number( baseDetails.basePrice - discountPrice ),
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 = {