tango-app-api-payment-subscription 3.5.24 → 3.5.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-payment-subscription",
3
- "version": "3.5.24",
3
+ "version": "3.5.25",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -430,11 +430,17 @@ export async function brandInvoiceList( req, res ) {
430
430
 
431
431
  // Invoice type sub-toggle: 'advance' shows only advance invoices (invoice #
432
432
  // starts with 'TINV-'); 'all' shows only regular invoices (NOT TINV-*).
433
- // Applied to the whole pipeline so list, count, summary and cards match.
433
+ // Applied for BOTH tango and client views. Uses $expr + $regexMatch (not the
434
+ // fragile { $not: <regex> } query operator, which was matching nothing and
435
+ // hiding every row) and treats a missing invoice # as a regular invoice.
434
436
  if ( req.body.invoiceType === 'advance' ) {
435
- query.push( { $match: { invoice: { $regex: '^TINV-' } } } );
437
+ query.push( { $match: { $expr: {
438
+ $regexMatch: { input: { $ifNull: [ '$invoice', '' ] }, regex: '^TINV-' },
439
+ } } } );
436
440
  } else if ( req.body.invoiceType === 'all' ) {
437
- query.push( { $match: { invoice: { $not: { $regex: '^TINV-' } } } } );
441
+ query.push( { $match: { $expr: {
442
+ $not: { $regexMatch: { input: { $ifNull: [ '$invoice', '' ] }, regex: '^TINV-' } },
443
+ } } } );
438
444
  }
439
445
 
440
446
  // Client users only ever see APPROVED invoices — the approval pipeline
@@ -460,7 +466,20 @@ export async function brandInvoiceList( req, res ) {
460
466
  dateFrom = now.subtract( 12, 'month' ).startOf( 'month' ).toDate();
461
467
  }
462
468
  if ( dateFrom ) {
463
- query.push( { $match: { billingDate: { $gte: dateFrom } } } );
469
+ // billingDate is stored as a STRING on many invoices (e.g.
470
+ // "2024-06-10T00:00:00.000Z"), so a plain { $gte: <Date> } compares
471
+ // string-vs-Date and matches nothing (this is why the client list came
472
+ // back empty on the default 'prev' filter). Coerce to a date first.
473
+ query.push( { $match: { $expr: {
474
+ $gte: [
475
+ { $cond: [
476
+ { $eq: [ { $type: '$billingDate' }, 'date' ] },
477
+ '$billingDate',
478
+ { $toDate: '$billingDate' },
479
+ ] },
480
+ dateFrom,
481
+ ],
482
+ } } } );
464
483
  }
465
484
  }
466
485
 
@@ -784,9 +803,22 @@ export async function latestDailyPricing( req, res ) {
784
803
  if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
785
804
  const sortKey = req.body.sortColumName;
786
805
  const sortDir = req.body.sortBy;
806
+ // Working days ('daysDifference' — the WORKING DAYS column) is not a
807
+ // top-level store field; it's per-product (products[].workingdays). So for
808
+ // that column sort by the store's MAX product working-days. Other columns
809
+ // (storeName, *CameraCount, zoneCount) sort by their top-level field.
810
+ const sortVal = ( s ) => {
811
+ if ( sortKey === 'daysDifference' ) {
812
+ return ( s.products || [] ).reduce(
813
+ ( mx, p ) => Math.max( mx, Number( p.workingdays ) || 0 ), 0 );
814
+ }
815
+ return s[sortKey];
816
+ };
787
817
  stores.sort( ( a, b ) => {
788
- if ( a[sortKey] < b[sortKey] ) return -1 * sortDir;
789
- if ( a[sortKey] > b[sortKey] ) return 1 * sortDir;
818
+ const av = sortVal( a );
819
+ const bv = sortVal( b );
820
+ if ( av < bv ) return -1 * sortDir;
821
+ if ( av > bv ) return 1 * sortDir;
790
822
  return 0;
791
823
  } );
792
824
  }
@@ -796,10 +828,16 @@ export async function latestDailyPricing( req, res ) {
796
828
  // BOTH the export and the on-screen table use the same numbers.
797
829
  const priceByProduct = {};
798
830
  let bpCurrency = 'inr';
831
+ // Step-pricing support: for STEP clients the per-store price depends on the
832
+ // store's 1-based POSITION within the product (tier ranges), so we can't use
833
+ // a single price per product. We pull the tiers here and resolve per-store
834
+ // prices by position below (mirrors invoice.controller's stepPrice).
835
+ let isStepClient = false;
836
+ const stepTiersByProduct = {};
799
837
  try {
800
838
  const bp = await basePriceService.findOne(
801
839
  { clientId: req.body.clientId },
802
- { standard: 1, currency: 1 },
840
+ { standard: 1, step: 1, currency: 1 },
803
841
  );
804
842
  bpCurrency = bp?.currency || 'inr';
805
843
  for ( const p of ( bp?.standard || [] ) ) {
@@ -808,9 +846,48 @@ export async function latestDailyPricing( req, res ) {
808
846
  priceByProduct[p.productName] = price;
809
847
  }
810
848
  }
849
+ // Client priceType drives whether we use step tiers.
850
+ const priceClient = await clientService.findOne(
851
+ { clientId: req.body.clientId }, { priceType: 1 } );
852
+ isStepClient = priceClient?.priceType === 'step';
853
+ if ( isStepClient ) {
854
+ for ( const p of ( bp?.step || [] ) ) {
855
+ if ( !p.productName ) {
856
+ continue;
857
+ }
858
+ if ( !stepTiersByProduct[p.productName] ) {
859
+ stepTiersByProduct[p.productName] = [];
860
+ }
861
+ stepTiersByProduct[p.productName].push( p );
862
+ }
863
+ // Tiers ordered by range start so the position lookup is correct.
864
+ for ( const name of Object.keys( stepTiersByProduct ) ) {
865
+ stepTiersByProduct[name].sort( ( a, b ) => {
866
+ const aStart = parseInt( String( a.storeRange || '0' ).split( '-' )[0], 10 ) || 0;
867
+ const bStart = parseInt( String( b.storeRange || '0' ).split( '-' )[0], 10 ) || 0;
868
+ return aStart - bStart;
869
+ } );
870
+ }
871
+ }
811
872
  } catch ( bpErr ) {
812
873
  logger.error( { error: bpErr, function: 'latestDailyPricing.basePrice', clientId: req.body.clientId } );
813
874
  }
875
+ // Per-store step price by 1-based position within the product. Falls back to
876
+ // the last tier's price beyond the defined ranges (mirrors invoice stepPrice).
877
+ const stepPriceForPosition = ( productName, positionInProduct ) => {
878
+ const tiers = stepTiersByProduct[productName] || [];
879
+ if ( !tiers.length ) {
880
+ return 0;
881
+ }
882
+ for ( const tier of tiers ) {
883
+ const [ min, max ] = String( tier.storeRange || '' ).split( '-' ).map( Number );
884
+ if ( positionInProduct >= min && positionInProduct <= max ) {
885
+ return Number( tier.negotiatePrice ) || Number( tier.basePrice ) || 0;
886
+ }
887
+ }
888
+ const last = tiers[tiers.length - 1];
889
+ return Number( last.negotiatePrice ) || Number( last.basePrice ) || 0;
890
+ };
814
891
  // Billing type per product (perStore / perZone / perCamera) from the client
815
892
  // plan — drives whether the amount multiplies by camera/zone count. The
816
893
  // same client doc supplies the invoice-amount currency below.
@@ -859,8 +936,31 @@ export async function latestDailyPricing( req, res ) {
859
936
  }
860
937
  return 1;
861
938
  };
939
+ // Step clients: resolve each store's per-product price by its 1-based
940
+ // POSITION within the product, over the CURRENT SORTED order (so re-sorting
941
+ // re-tiers the amounts). Built here — after the sort, over the FULL list —
942
+ // so a store on page 2 still gets the position determined by all preceding
943
+ // stores. Keyed `storeId||productName`. Standard clients use priceByProduct.
944
+ const stepPriceByStoreProduct = {};
945
+ if ( isStepClient ) {
946
+ const positionByProduct = {};
947
+ for ( const s of stores ) {
948
+ for ( const p of ( s.products || [] ) ) {
949
+ const name = p.productName;
950
+ positionByProduct[name] = ( positionByProduct[name] || 0 ) + 1;
951
+ stepPriceByStoreProduct[`${s.storeId}||${name}`] =
952
+ stepPriceForPosition( name, positionByProduct[name] );
953
+ }
954
+ }
955
+ }
956
+ const priceForStoreProduct = ( store, product ) => {
957
+ if ( isStepClient ) {
958
+ return stepPriceByStoreProduct[`${store.storeId}||${product.productName}`] || 0;
959
+ }
960
+ return priceByProduct[product.productName] || 0;
961
+ };
862
962
  const productInvoiceAmount = ( store, product ) => {
863
- const price = priceByProduct[product.productName] || 0;
963
+ const price = priceForStoreProduct( store, product );
864
964
  const workingDays = Number( product.workingdays ) || 0;
865
965
  if ( price <= 0 || workingDays <= 0 ) {
866
966
  return 0;
@@ -935,7 +1035,7 @@ export async function latestDailyPricing( req, res ) {
935
1035
  invoiceBreakdown.push( {
936
1036
  productName: product.productName,
937
1037
  label: prettyProduct( product.productName ),
938
- price: priceByProduct[product.productName] || 0,
1038
+ price: priceForStoreProduct( store, product ),
939
1039
  workingDays: Number( product.workingdays ) || 0,
940
1040
  amount,
941
1041
  } );
@@ -1847,6 +1947,23 @@ export async function getInrRate( currency ) {
1847
1947
  return FALLBACK_INR[iso] || 1;
1848
1948
  }
1849
1949
 
1950
+ // FX rates for the app's supported currencies, expressed as INR-per-1-unit.
1951
+ // The UI converts an amount from currency A to B as: amount * (rates[A] / rates[B]).
1952
+ // Reuses the cached live rates (getInrRate); inr is always 1.
1953
+ export async function fxRates( req, res ) {
1954
+ try {
1955
+ const codes = [ 'inr', 'dollar', 'singaporedollar', 'euro', 'aed' ];
1956
+ const rates = {};
1957
+ for ( const c of codes ) {
1958
+ rates[c] = await getInrRate( c );
1959
+ }
1960
+ return res.sendSuccess( { rates } );
1961
+ } catch ( error ) {
1962
+ logger.error( { error: error, function: 'fxRates' } );
1963
+ return res.sendError( error, 500 );
1964
+ }
1965
+ }
1966
+
1850
1967
  export async function billingSummary( req, res ) {
1851
1968
  try {
1852
1969
  const now = dayjs();
@@ -1,6 +1,6 @@
1
1
 
2
2
  import express from 'express';
3
- import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts, additionalProductExport } from '../controllers/brandsBilling.controller.js';
3
+ import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts, additionalProductExport, fxRates } from '../controllers/brandsBilling.controller.js';
4
4
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
5
5
 
6
6
  export const brandsBillingRouter = express.Router();
@@ -11,10 +11,11 @@ brandsBillingRouter.post( '/latestDailyPricing', isAllowedSessionHandler, access
11
11
  brandsBillingRouter.post( '/brandBillingGroups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), brandBillingGroups );
12
12
  brandsBillingRouter.put( '/updateDailyPricingWorkingDays', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingWorkingDays );
13
13
  brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingStoreField );
14
- brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), getClientBillingInfo );
14
+ brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [] } ] } ), getClientBillingInfo );
15
15
  brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
16
16
  brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [ 'isEdit' ] } ] } ), bulkUpdateBillingGroups );
17
17
  brandsBillingRouter.post( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), billingSummary );
18
18
  brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), billingSummary );
19
19
  brandsBillingRouter.get( '/additionalProducts', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), additionalProducts );
20
20
  brandsBillingRouter.get( '/additionalProductExport', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), additionalProductExport );
21
+ brandsBillingRouter.get( '/fxRates', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ], access: [ { featureName: 'TangoAdmin', name: 'EditBilling', permissions: [] } ] } ), fxRates );