tango-app-api-payment-subscription 3.5.24 → 3.5.26
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
|
@@ -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
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
789
|
-
|
|
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 =
|
|
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:
|
|
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();
|
|
@@ -1280,7 +1280,11 @@ async function resolveBasePricingScope( group, getClient ) {
|
|
|
1280
1280
|
|
|
1281
1281
|
async function standardPrice( group, getClient, baseDate ) {
|
|
1282
1282
|
console.log( '🚀 ~ standardPrice ~ baseDate:', baseDate.format( 'MMM YYYY' ) );
|
|
1283
|
-
|
|
1283
|
+
// Prorate against the actual number of days in the invoice's billing month
|
|
1284
|
+
// (e.g. 31 for July, 28/29 for Feb) so invoice line amounts match the
|
|
1285
|
+
// annexure, which uses billingMonth.daysInMonth() in buildAnnexureRows().
|
|
1286
|
+
// Previously hardcoded to 30, which inflated 31-day-month invoices.
|
|
1287
|
+
const currentMonthDays = baseDate.daysInMonth();
|
|
1284
1288
|
console.log( '🚀 ~ standardPrice ~ currentMonthDays:', currentMonthDays );
|
|
1285
1289
|
// Pricing method: 'flat' => bill every store for the full month
|
|
1286
1290
|
// regardless of working days. 'prorate' => bill for actual working days.
|
|
@@ -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: '
|
|
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 );
|