tango-app-api-payment-subscription 3.5.12 → 3.5.14

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node --check src/controllers/paymentSubscription.controllers.js)",
5
+ "Bash(npx eslint *)",
6
+ "Bash(echo \"---eslint-exit:$?---\")"
7
+ ]
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-payment-subscription",
3
- "version": "3.5.12",
3
+ "version": "3.5.14",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,201 @@
1
+ // One-shot script: creates billing groups by store country for specific
2
+ // clients. For each target client it reads every store, groups storeIds by
3
+ // `storeProfile.country`, and creates one billing group per distinct country
4
+ // holding all the storeIds in that country.
5
+ //
6
+ // Scope: ONLY clientIds 387 and 193 (override with --clients=a,b).
7
+ //
8
+ // Idempotency / existing groups: if a billing group with the same
9
+ // (clientId, groupName=<country>) already exists, its `stores` array is
10
+ // OVERWRITTEN to match the current country membership (not duplicated).
11
+ //
12
+ // One store, one group: a store placed in a country group is pulled out of the
13
+ // primary "Default Group" so it lives in exactly one billing group (mirrors
14
+ // createBillingGroup).
15
+ //
16
+ // Stores with a missing/empty country are NOT grouped — they remain in the
17
+ // primary "Default Group". Any legacy fallback group named UNKNOWN_GROUP_NAME
18
+ // (from an earlier version of this script) is deleted; its stores already live
19
+ // in the Default Group, so nothing is lost.
20
+ //
21
+ // Connection: reuses the app's own getConnection() (config/database) so it
22
+ // connects exactly like the server does — reading mongo_username /
23
+ // mongo_password from .env plus host/name/authSource from config/env/env.js.
24
+ // Override with MONGO_URI if you want to point it elsewhere.
25
+ //
26
+ // Run modes:
27
+ // DRY RUN (default) — prints what it WOULD create/update, writes nothing:
28
+ // node scripts/create-billing-groups-by-country.js
29
+ // APPLY — actually writes:
30
+ // node scripts/create-billing-groups-by-country.js --apply
31
+ //
32
+ // Other flags:
33
+ // --clients=387,193 override the target client list
34
+
35
+ import mongoose from 'mongoose';
36
+ import 'dotenv/config';
37
+ import { getConnection } from '../config/database/database.js';
38
+
39
+ const DEFAULT_CLIENTS = [ '387', '193' ];
40
+
41
+ // Fallback billing group for stores that have no storeProfile.country.
42
+ const UNKNOWN_GROUP_NAME = 'Unknown';
43
+
44
+ function parseArgs( argv ) {
45
+ const apply = argv.includes( '--apply' ) || process.env.APPLY === 'true';
46
+ let clients = DEFAULT_CLIENTS;
47
+ const clientsArg = argv.find( ( a ) => a.startsWith( '--clients=' ) );
48
+ if ( clientsArg ) {
49
+ clients = clientsArg.slice( '--clients='.length ).split( ',' ).map( ( c ) => c.trim() ).filter( Boolean );
50
+ }
51
+ return { apply, clients };
52
+ }
53
+
54
+ async function run() {
55
+ const { apply, clients } = parseArgs( process.argv.slice( 2 ) );
56
+
57
+ console.log( `Target clients: ${clients.join( ', ' )}` );
58
+ console.log( `Mode: ${apply ? 'APPLY (writing to DB)' : 'DRY RUN (no writes)'}` );
59
+ console.log( '' );
60
+
61
+ // Prefer an explicit MONGO_URI override; otherwise build the connection the
62
+ // same way the app does via getConnection().
63
+ if ( process.env.MONGO_URI ) {
64
+ await mongoose.connect( process.env.MONGO_URI );
65
+ } else {
66
+ const { uri, options } = getConnection();
67
+ await mongoose.connect( uri, options );
68
+ }
69
+
70
+ // strict:false so we don't pin to a specific tango-api-schema version.
71
+ const Store = mongoose.model(
72
+ '_cbgStore',
73
+ new mongoose.Schema( {}, { strict: false } ),
74
+ 'stores',
75
+ );
76
+ const Billing = mongoose.model(
77
+ '_cbgBilling',
78
+ new mongoose.Schema( {}, { strict: false } ),
79
+ 'billings',
80
+ );
81
+
82
+ let totalGroupsCreated = 0;
83
+ let totalGroupsUpdated = 0;
84
+ let totalPulledFromPrimary = 0;
85
+ let totalUnknownGroupsDeleted = 0;
86
+ const noCountryStores = [];
87
+
88
+ for ( const clientId of clients ) {
89
+ const stores = await Store.find(
90
+ { clientId: clientId },
91
+ { 'storeId': 1, 'storeName': 1, 'storeProfile.country': 1 },
92
+ ).lean();
93
+
94
+ console.log( `=== Client ${clientId}: ${stores.length} store(s) ===` );
95
+
96
+ if ( !stores.length ) {
97
+ console.log( ' No stores found. Skipping.\n' );
98
+ continue;
99
+ }
100
+
101
+ // Group storeIds by country. Stores with no country are NOT grouped — they
102
+ // remain in the primary "Default Group".
103
+ const byCountry = new Map();
104
+ for ( const s of stores ) {
105
+ const country = ( s.storeProfile?.country || '' ).trim();
106
+ if ( !country ) {
107
+ noCountryStores.push( { clientId, storeId: s.storeId, storeName: s.storeName } );
108
+ continue;
109
+ }
110
+ if ( !byCountry.has( country ) ) {
111
+ byCountry.set( country, [] );
112
+ }
113
+ byCountry.get( country ).push( s.storeId );
114
+ }
115
+
116
+ // Tidy up the legacy fallback group: country-less stores belong in the
117
+ // primary group, so any previously-created "Unknown" group is removed.
118
+ // (Its stores are also in the primary group, so nothing is lost.)
119
+ const unknownGroup = await Billing.findOne( { clientId: clientId, groupName: UNKNOWN_GROUP_NAME } );
120
+ if ( unknownGroup ) {
121
+ console.log( ` [DELETE] legacy "${UNKNOWN_GROUP_NAME}" group (_id ${unknownGroup._id}) — country-less stores stay in Default Group` );
122
+ if ( apply ) {
123
+ await Billing.deleteOne( { _id: unknownGroup._id } );
124
+ }
125
+ totalUnknownGroupsDeleted++;
126
+ }
127
+
128
+ if ( !byCountry.size ) {
129
+ console.log( ' No stores with a country value. Nothing to group.\n' );
130
+ continue;
131
+ }
132
+
133
+ for ( const [ country, storeIds ] of byCountry ) {
134
+ const existing = await Billing.findOne( { clientId: clientId, groupName: country } );
135
+
136
+ if ( existing ) {
137
+ console.log( ` [UPDATE] group "${country}" exists (_id ${existing._id}) → set ${storeIds.length} store(s)` );
138
+ if ( apply ) {
139
+ await Billing.updateOne(
140
+ { _id: existing._id },
141
+ { $set: { stores: storeIds } },
142
+ );
143
+ }
144
+ totalGroupsUpdated++;
145
+ } else {
146
+ console.log( ` [CREATE] group "${country}" with ${storeIds.length} store(s)` );
147
+ if ( apply ) {
148
+ await Billing.create( {
149
+ clientId: clientId,
150
+ groupName: country,
151
+ groupTag: 'store',
152
+ country: country,
153
+ stores: storeIds,
154
+ } );
155
+ }
156
+ totalGroupsCreated++;
157
+ }
158
+
159
+ // A store lives in exactly one group: once it's in a country group, pull
160
+ // it out of the primary "Default Group" (mirrors createBillingGroup).
161
+ if ( apply ) {
162
+ const pullRes = await Billing.updateOne(
163
+ { clientId: clientId, isPrimary: true },
164
+ { $pull: { stores: { $in: storeIds } } },
165
+ );
166
+ if ( pullRes.modifiedCount ) {
167
+ totalPulledFromPrimary += storeIds.length;
168
+ }
169
+ } else {
170
+ totalPulledFromPrimary += storeIds.length;
171
+ }
172
+ }
173
+ console.log( ` → pulled country-matched stores out of primary "Default Group"` );
174
+ console.log( '' );
175
+ }
176
+
177
+ console.log( '--- Summary ---' );
178
+ console.log( `Groups created: ${totalGroupsCreated}` );
179
+ console.log( `Groups updated: ${totalGroupsUpdated}` );
180
+ console.log( `Legacy "${UNKNOWN_GROUP_NAME}" groups deleted: ${totalUnknownGroupsDeleted}` );
181
+ console.log( `Store memberships pulled from primary group: ${totalPulledFromPrimary}` );
182
+ if ( noCountryStores.length ) {
183
+ console.log( `Stores with no country (left in "Default Group"): ${noCountryStores.length}` );
184
+ noCountryStores.slice( 0, 50 ).forEach( ( s ) =>
185
+ console.log( ` - client ${s.clientId} | ${s.storeId} | ${s.storeName || ''}` ),
186
+ );
187
+ if ( noCountryStores.length > 50 ) {
188
+ console.log( ` ...and ${noCountryStores.length - 50} more` );
189
+ }
190
+ }
191
+ if ( !apply ) {
192
+ console.log( '\nDRY RUN complete. No data was written. Re-run with --apply to write.' );
193
+ }
194
+
195
+ await mongoose.disconnect();
196
+ }
197
+
198
+ run().catch( ( err ) => {
199
+ console.error( err );
200
+ process.exit( 1 );
201
+ } );
@@ -586,16 +586,34 @@ export async function brandInvoiceList( req, res ) {
586
586
  };
587
587
 
588
588
  if ( req.body.export ) {
589
+ // Mirror the on-screen table exactly: same columns (excl. GST and incl.
590
+ // GST as separate amounts), same currency symbol, and the same status
591
+ // text the table renders via getInvoiceStatus().
592
+ const currencySymbols = {
593
+ inr: '₹', dollar: '$', singaporedollar: 'S$', euro: '€', aed: 'AED',
594
+ };
595
+ const statusFor = ( inv ) => {
596
+ if ( inv.status === 'pendingCsm' ) return 'Pending CSM';
597
+ if ( inv.status === 'pendingFinance' ) return 'Pending Finance';
598
+ if ( inv.status === 'pendingApproval' ) return 'Pending Approval';
599
+ if ( inv.status === 'pending' ) return 'Pending Approval';
600
+ if ( inv.paymentStatus === 'partial' ) return 'Partial';
601
+ if ( inv.paymentStatus === 'unpaid' ) return 'Pending Payment';
602
+ if ( inv.paymentStatus === 'paid' ) return 'Paid';
603
+ return inv.status;
604
+ };
589
605
  const exportdata = [];
590
606
  allInvoices.forEach( ( element ) => {
607
+ const symbol = currencySymbols[element.currency] || '$';
591
608
  exportdata.push( {
592
609
  'Invoice #': element.invoice,
593
610
  'Billing Group': element.groupName,
594
611
  'Period': dayjs( element.billingDate ).format( 'MMM YYYY' ),
595
612
  'Generated': dayjs( element.billingDate ).format( 'DD MMM YYYY' ),
596
613
  'No of Stores': element.stores,
597
- 'Amount': element.totalAmount,
598
- 'Status': [ 'pendingCsm', 'pendingFinance', 'pendingApproval' ].includes( element.status ) ? 'Pending Approval' : element.paymentStatus === 'unpaid' ? 'Pending Payment' : 'Paid',
614
+ 'Amount (excl. GST)': `${symbol}${Number( element.amount || 0 ).toFixed( 2 )}`,
615
+ 'Amount (incl. GST)': `${symbol}${Number( element.totalAmount || 0 ).toFixed( 2 )}`,
616
+ 'Status': statusFor( element ),
599
617
  } );
600
618
  } );
601
619
  await download( exportdata, res );
@@ -705,18 +723,27 @@ export async function latestDailyPricing( req, res ) {
705
723
  logger.error( { error: bpErr, function: 'latestDailyPricing.basePrice', clientId: req.body.clientId } );
706
724
  }
707
725
  // Billing type per product (perStore / perZone / perCamera) from the client
708
- // plan — drives whether the amount multiplies by camera/zone count.
726
+ // plan — drives whether the amount multiplies by camera/zone count. The
727
+ // same client doc supplies the invoice-amount currency below.
709
728
  const billingTypeByProduct = {};
729
+ // Invoice Amount currency comes from the client's paymentInvoice.currencyType
730
+ // (normalised to 'dollar' / 'inr' like the rest of this file). Falls back to
731
+ // the base-pricing currency only when the client has no currencyType set.
732
+ let invoiceCurrency = bpCurrency;
710
733
  try {
711
734
  const planClient = await clientService.findOne(
712
735
  { clientId: req.body.clientId },
713
- { 'planDetails.product.productName': 1, 'planDetails.product.billingType': 1 },
736
+ { 'planDetails.product.productName': 1, 'planDetails.product.billingType': 1, 'paymentInvoice.currencyType': 1 },
714
737
  );
715
738
  for ( const p of ( planClient?.planDetails?.product || [] ) ) {
716
739
  if ( p.productName ) {
717
740
  billingTypeByProduct[p.productName] = p.billingType || 'perStore';
718
741
  }
719
742
  }
743
+ const clientCurrency = planClient?.paymentInvoice?.currencyType;
744
+ if ( clientCurrency ) {
745
+ invoiceCurrency = clientCurrency === 'dollar' ? 'dollar' : 'inr';
746
+ }
720
747
  } catch ( btErr ) {
721
748
  logger.error( { error: btErr, function: 'latestDailyPricing.billingType', clientId: req.body.clientId } );
722
749
  }
@@ -828,7 +855,7 @@ export async function latestDailyPricing( req, res ) {
828
855
  ...( store._doc || store ),
829
856
  invoiceAmount: Math.round( invoiceAmount * 100 ) / 100,
830
857
  invoiceBreakdown,
831
- invoiceCurrency: bpCurrency,
858
+ invoiceCurrency: invoiceCurrency,
832
859
  };
833
860
  } );
834
861
 
@@ -324,7 +324,15 @@ export async function createInvoice( req, res ) {
324
324
  $filter: {
325
325
  input: '$stores',
326
326
  as: 'item',
327
- cond: { $and: [ { $gt: [ '$$item.daysDifference', 0 ] }, { $in: [ '$$item.storeId', group.stores ] } ] },
327
+ // A store is "running" if it has working days for ANY product.
328
+ // Edge products (tangoZone/tangoTraffic) use daysDifference;
329
+ // tangoTrax uses daysDifferenceTrax. A trax-only store has
330
+ // daysDifference = 0 but daysDifferenceTrax > 0 — counting only
331
+ // daysDifference would wrongly drop it and report stores: 0.
332
+ cond: { $and: [
333
+ { $or: [ { $gt: [ '$$item.daysDifference', 0 ] }, { $gt: [ '$$item.daysDifferenceTrax', 0 ] } ] },
334
+ { $in: [ '$$item.storeId', group.stores ] },
335
+ ] },
328
336
  },
329
337
  },
330
338
  },
@@ -440,10 +448,11 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
440
448
  const billingMonthEnd = new Date( billingMonth.endOf( 'month' ).toISOString() );
441
449
  const monthDays = billingMonth.daysInMonth();
442
450
  const invoiceCurrency = symbolFor( invoiceInfo.currency );
443
- // basepricing negotiatePrice is stored in INR. For non-INR invoices the
444
- // annexure must convert it to the invoice currency, otherwise the INR number
445
- // is shown verbatim under a $ symbol (e.g. ₹1650 rendered as "$1,650").
446
- const annexFx = invoiceInfo.currency === 'dollar' ? ( await getUsdInrRate() ) : 1;
451
+ // basepricing negotiatePrice is stored in the client's own billing currency
452
+ // (INR for INR clients, the foreign currency for dollar/AED clients). Invoice
453
+ // generation uses negotiatePrice verbatim (no FX), so the annexure must too
454
+ // converting here would make the annexure unit price disagree with the actual
455
+ // billed amount (e.g. a $45 negotiated price wrongly shown as $0.48).
447
456
 
448
457
  const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1 } );
449
458
  const billingTypeMap = {};
@@ -504,9 +513,9 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
504
513
  units = s.trafficCameraCount;
505
514
  }
506
515
  }
507
- // Convert the INR negotiatePrice into the invoice currency (annexFx = 1 for
508
- // INR invoices, = USD→INR rate for dollar invoices, so divide).
509
- const price = ( Number( s.standard?.negotiatePrice ) || 0 ) / annexFx;
516
+ // negotiatePrice is already in the invoice currency use it verbatim,
517
+ // matching invoice generation.
518
+ const price = Number( s.standard?.negotiatePrice ) || 0;
510
519
  const runningCost = s.workingdays >= monthDays ?
511
520
  Math.round( price * units * 100 ) / 100 :
512
521
  Math.round( ( price / monthDays ) * s.workingdays * units * 100 ) / 100;
@@ -1713,7 +1722,6 @@ async function stepPrice( group, getClient ) {
1713
1722
  function processArray( array1, array2 ) {
1714
1723
  let updatedArray = [];
1715
1724
 
1716
- let firstPriceAssigned = false;
1717
1725
  for ( let item of array1 ) {
1718
1726
  let remainingStores = item.storeCount;
1719
1727
 
@@ -1721,16 +1729,17 @@ async function stepPrice( group, getClient ) {
1721
1729
  let [ min, max ] = range.storeRange.split( '-' ).map( Number );
1722
1730
 
1723
1731
  if ( remainingStores > 0 ) {
1724
- console.log( firstPriceAssigned );
1725
1732
  let applicableStores = Math.min( remainingStores, max - min + 1 );
1726
1733
  updatedArray.push( {
1727
1734
  productName: item.productName,
1728
1735
  workingdays: item.workingdays,
1729
1736
  storeCount: applicableStores,
1730
- price: firstPriceAssigned ? 1100 : range.negotiatePrice,
1737
+ // Each tier is charged at its own range's negotiated price. (Was a
1738
+ // hardcoded 1100 for every tier after the first — a placeholder
1739
+ // that ignored the actual step price of the next range.)
1740
+ price: range.negotiatePrice,
1731
1741
  } );
1732
1742
  remainingStores -= applicableStores;
1733
- firstPriceAssigned = true;
1734
1743
  }
1735
1744
  }
1736
1745
  }
@@ -3325,13 +3325,201 @@ export const dailyPricingInsertOld = async ( req, res ) => {
3325
3325
  }
3326
3326
  };
3327
3327
 
3328
+ // Computes the daily-pricing entry for a single store. Pure per-store work
3329
+ // (independent DB reads + calculation) so stores can run concurrently.
3330
+ // storeIndex is the store's original position in getStore — required for the
3331
+ // step-pricing storeRange check, so it must be preserved when parallelizing.
3332
+ const processStoreDailyPricing = async ( store, storeIndex, getClient, getBaseprice, date ) => {
3333
+ let productList = [];
3334
+ let query = [
3335
+ {
3336
+ $match: {
3337
+ clientId: getClient.clientId,
3338
+ },
3339
+ },
3340
+ { $unwind: '$stores' },
3341
+ {
3342
+ $match: {
3343
+ 'stores.storeId': store.storeId,
3344
+ },
3345
+ },
3346
+ {
3347
+ $project: {
3348
+ 'stores.daysDifference': 1,
3349
+ 'stores.daysDifferenceTrax': 1,
3350
+ 'dateString': 1,
3351
+ },
3352
+ },
3353
+ {
3354
+ $sort: {
3355
+ _id: -1,
3356
+ },
3357
+ },
3358
+ { $limit: 1 },
3359
+ ];
3360
+ let dailyData = await dailyPriceService.aggregate( query );
3361
+ let cameraDetails = await cameraService.find( { storeId: store.storeId, clientId: getClient.clientId, isActivated: true, isUp: true }, { streamName: 1, productModule: 1 } );
3362
+ // console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3363
+
3364
+ let trafficCameraCount = cameraDetails.filter( ( cam ) =>
3365
+ ( cam.productModule || [] ).some( ( mod ) =>
3366
+ ( mod.productName === 'tangoTraffic' || mod.productName === 'tangoTracking' ) && mod.checked === true,
3367
+ ),
3368
+ ).length;
3369
+
3370
+ let zoneCameraCount = cameraDetails.filter( ( cam ) =>
3371
+ ( cam.productModule || [] ).some( ( mod ) =>
3372
+ mod.productName === 'tangoZone' && mod.checked === true,
3373
+ ),
3374
+ ).length;
3375
+
3376
+ let zoneCameraStreamNames = cameraDetails.filter( ( cam ) =>
3377
+ ( cam.productModule || [] ).some( ( mod ) =>
3378
+ mod.productName === 'tangoZone' && mod.checked === true,
3379
+ ),
3380
+ ).map( ( cam ) => cam.streamName );
3381
+ let allcameraname = cameraDetails?.map( ( cam ) => cam?.streamName );
3382
+ console.log( '🚀 ~ dailyPricingInsert ~ zoneCameraStreamNames:', zoneCameraStreamNames );
3383
+ let taggingDetails = await taggingService.find( { storeId: store.storeId, clientId: getClient.clientId, productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: allcameraname } }, { tagName: 1 } );
3384
+ let zoneCount = taggingDetails.length;
3385
+ let zoneName = taggingDetails.map( ( item ) => item.tagName );
3386
+ let firstDate = dayjs( store?.edge?.firstFileDate ).format( 'YYYY-MM-DD' );
3387
+ let workingdays;
3388
+ let workingdaystrax;
3389
+ const givenDate = dayjs( date );
3390
+ console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', store.storeId, trafficCameraCount, zoneCameraCount, zoneCount );
3391
+ const isFirstDayOfMonth = givenDate.isSame( dayjs().startOf( 'month' ), 'day' );
3392
+ if ( store?.edge.firstFile ) {
3393
+ if ( firstDate < date && store?.status == 'active' &&
3394
+ dailyData[0]?.dateString != dayjs( date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) ) {
3395
+ if ( dailyData.length && dailyData[0]?.stores?.daysDifference&&!isFirstDayOfMonth ) {
3396
+ workingdays = dailyData[0]?.stores?.daysDifference + 1;
3397
+ } else {
3398
+ workingdays = 1;
3399
+ }
3400
+ } else {
3401
+ if ( dailyData[0]?.stores?.daysDifference&&!isFirstDayOfMonth ) {
3402
+ workingdays = dailyData[0]?.stores?.daysDifference;
3403
+ } else {
3404
+ workingdays = 0;
3405
+ }
3406
+ }
3407
+ }
3408
+ console.log( dailyData[0]?.stores );
3409
+ if ( store?.status == 'active' &&
3410
+ dailyData[0]?.dateString != dayjs( date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) ) {
3411
+ if ( dailyData.length && dailyData[0]?.stores?.daysDifferenceTrax&&!isFirstDayOfMonth ) {
3412
+ workingdaystrax = dailyData[0]?.stores?.daysDifferenceTrax + 1;
3413
+ } else {
3414
+ workingdaystrax = 1;
3415
+ }
3416
+ } else {
3417
+ if ( dailyData[0]?.stores?.daysDifferenceTrax&&!isFirstDayOfMonth ) {
3418
+ workingdaystrax = dailyData[0]?.stores?.daysDifferenceTrax;
3419
+ } else {
3420
+ workingdaystrax = 0;
3421
+ }
3422
+ }
3423
+ let priceDetails = getClient.priceType == 'standard' ? getBaseprice.standard : getBaseprice.step;
3424
+ priceDetails = priceDetails.filter( ( item1 ) =>
3425
+ getClient.planDetails.product.some( ( item2 ) =>
3426
+ item2.productName === item1.productName && item2.status === 'live',
3427
+ ),
3428
+ );
3429
+ for ( let storeProductIndex = 0; storeProductIndex < store.product.length; storeProductIndex++ ) {
3430
+ let productDetails;
3431
+ if ( getClient.priceType == 'standard' ) {
3432
+ productDetails = priceDetails.find( ( item ) => item.productName == store.product[storeProductIndex] );
3433
+ } else {
3434
+ productDetails = priceDetails.find( ( item ) => {
3435
+ let range = item.storeRange.split( '-' );
3436
+ if ( parseInt( range[0] ) <= ( storeIndex + 1 ) && parseInt( range[1] ) >= ( storeIndex + 1 ) ) {
3437
+ return ( item.productName == store.product[storeProductIndex] && parseInt( range[0] ) <= ( storeIndex + 1 ) && parseInt( range[1] ) >= ( storeIndex + 1 ) );
3438
+ }
3439
+ } );
3440
+ if ( !productDetails ) {
3441
+ let stepProductDetails = priceDetails.filter( ( item ) => item.productName == store.product[storeProductIndex] );
3442
+ productDetails = stepProductDetails[stepProductDetails.length - 1];
3443
+ }
3444
+ }
3445
+ if ( productDetails ) {
3446
+ console.log( '-------', workingdays, workingdaystrax, productDetails.productName );
3447
+ let newObject = {
3448
+ productName: productDetails.productName,
3449
+ workingdays: productDetails.productName==='tangoTrax'?workingdaystrax:workingdays,
3450
+ };
3451
+ productList.push( newObject );
3452
+ }
3453
+ }
3454
+ return {
3455
+ storeId: store.storeId,
3456
+ store: store._id,
3457
+ storeName: store.storeName,
3458
+ status: store?.status,
3459
+ firstFile: store?.edge?.firstFile || false,
3460
+ edgefirstFileDate: store?.edge?.firstFileDate || null,
3461
+ date: new Date( date ),
3462
+ daysDifference: workingdays,
3463
+ daysDifferenceTrax: workingdaystrax,
3464
+ products: productList,
3465
+ camera: cameraDetails.map( ( item ) => item.streamName ),
3466
+ trafficCameraCount: trafficCameraCount,
3467
+ zoneCameraCount: zoneCameraCount,
3468
+ zoneCount: zoneCount,
3469
+ zoneName: zoneName,
3470
+ };
3471
+ };
3472
+
3473
+ // Processes daily pricing for a single client. Self-contained so multiple
3474
+ // clients can run concurrently via Promise.all in dailyPricingInsert.
3475
+ const processClientDailyPricing = async ( clientId, date ) => {
3476
+ let getClient = await paymentService.findOne( { clientId: clientId } );
3477
+ if ( getClient ) {
3478
+ let getBaseprice = await basePriceService.findOne( { clientId: clientId } );
3479
+ let getStore = await storeService.find( { 'clientId': clientId } );
3480
+ console.log( '==========>', clientId, getStore.length );
3481
+ if ( getStore.length && getBaseprice ) {
3482
+ // Process stores in bounded batches so large clients (200+ stores) run
3483
+ // in parallel without exhausting the DB connection pool. storeIndex is
3484
+ // preserved so step-pricing ranges still resolve correctly.
3485
+ const STORE_BATCH_SIZE = 20;
3486
+ let storeList = [];
3487
+ for ( let i = 0; i < getStore.length; i += STORE_BATCH_SIZE ) {
3488
+ let batch = getStore.slice( i, i + STORE_BATCH_SIZE );
3489
+ let batchResults = await Promise.all(
3490
+ batch.map( ( store, batchIndex ) =>
3491
+ processStoreDailyPricing( store, i + batchIndex, getClient, getBaseprice, date ),
3492
+ ),
3493
+ );
3494
+ storeList.push( ...batchResults );
3495
+ }
3496
+
3497
+ let activestores = storeList.filter( ( store ) => store.status=='active' );
3498
+ console.log( '------', activestores.length );
3499
+ let params = {
3500
+ clientId: clientId,
3501
+ stores: storeList,
3502
+ dateISO: new Date( date ),
3503
+ accountType: getClient?.planDetails?.subscriptionType,
3504
+ status: getClient?.status,
3505
+ activeStores: activestores?.length,
3506
+ brandName: getClient?.clientName,
3507
+ proRate: getClient?.paymentInvoice?.proRate,
3508
+ dateString: dayjs( date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ),
3509
+ };
3510
+ await dailyPriceService.updateOne( { clientId: clientId, dateString: params.dateString }, params, { upsert: true } );
3511
+ } else {
3512
+ // console.log( 'store or base price not found' );
3513
+ }
3514
+ }
3515
+ };
3516
+
3328
3517
  export const dailyPricingInsert = async ( req, res ) => {
3329
3518
  try {
3330
3519
  let requestData = req.body;
3331
- let clientlist;
3332
3520
  let requestClient = [];
3333
3521
  if ( !requestData?.clientId || !requestData?.clientId?.length ) {
3334
- clientlist = await paymentService.find( { 'status': 'active' } );
3522
+ let clientlist = await paymentService.find( { 'status': 'active' } );
3335
3523
  for ( let client of clientlist ) {
3336
3524
  requestClient.push( client.clientId );
3337
3525
  }
@@ -3342,214 +3530,45 @@ export const dailyPricingInsert = async ( req, res ) => {
3342
3530
  if ( !requestData?.date ) {
3343
3531
  requestData.date = dayjs().format( 'YYYY-MM-DD' );
3344
3532
  }
3345
- if ( requestData.clientId && requestClient.length > 0 ) {
3346
- for ( let clientIndex = 0; clientIndex < requestClient.length; clientIndex++ ) {
3347
- let getClient = await paymentService.findOne( { clientId: requestClient[clientIndex] } );
3348
- if ( getClient ) {
3349
- let getBaseprice = await basePriceService.findOne( { clientId: requestClient[clientIndex] } );
3350
- let getStore = await storeService.find( { 'clientId': requestClient[clientIndex] } );
3351
- console.log( '==========>', getStore.length );
3352
- if ( getStore.length ) {
3353
- let storeList = [];
3354
- for ( let storeIndex = 0; storeIndex < getStore.length; storeIndex++ ) {
3355
- let productList = [];
3356
- if ( getBaseprice ) {
3357
- // await axios.get( `${JSON.parse( process.env.URL ).oldapidomain}/processedDayData/getDailyData?clientId=${requestClient[clientIndex]}&storeId=${getStore[storeIndex].storeId}&date=${requestData.date}`, { headers: { Authorization: 'Bearer d47433f8-9a33-47c7-ba43-1a0fbac28f66' } } ).then( async ( response ) => {
3358
- // let processedFileDate = response.data?.data?.firstFileDate || null;
3359
- let query = [
3360
- {
3361
- $match: {
3362
- clientId: getClient.clientId,
3363
- },
3364
- },
3365
- { $unwind: '$stores' },
3366
- {
3367
- $match: {
3368
- 'stores.storeId': getStore[storeIndex].storeId,
3369
- },
3370
- },
3371
- {
3372
- $project: {
3373
- 'stores.daysDifference': 1,
3374
- 'stores.daysDifferenceTrax': 1,
3375
- 'dateString': 1,
3376
- },
3377
- },
3378
- {
3379
- $sort: {
3380
- _id: -1,
3381
- },
3382
- },
3383
- { $limit: 1 },
3384
- ];
3385
- let dailyData = await dailyPriceService.aggregate( query );
3386
- let cameraDetails = await cameraService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], isActivated: true, isUp: true }, { streamName: 1, productModule: 1 } );
3387
- // console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3388
-
3389
- let trafficCameraCount = cameraDetails.filter( ( cam ) =>
3390
- ( cam.productModule || [] ).some( ( mod ) =>
3391
- ( mod.productName === 'tangoTraffic' || mod.productName === 'tangoTracking' ) && mod.checked === true,
3392
- ),
3393
- ).length;
3394
-
3395
- let zoneCameraCount = cameraDetails.filter( ( cam ) =>
3396
- ( cam.productModule || [] ).some( ( mod ) =>
3397
- mod.productName === 'tangoZone' && mod.checked === true,
3398
- ),
3399
- ).length;
3400
-
3401
- let zoneCameraStreamNames = cameraDetails.filter( ( cam ) =>
3402
- ( cam.productModule || [] ).some( ( mod ) =>
3403
- mod.productName === 'tangoZone' && mod.checked === true,
3404
- ),
3405
- ).map( ( cam ) => cam.streamName );
3406
- let allcameraname = cameraDetails?.map( ( cam ) => cam?.streamName );
3407
- console.log( '🚀 ~ dailyPricingInsert ~ zoneCameraStreamNames:', zoneCameraStreamNames );
3408
- let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: allcameraname } }, { tagName: 1 } );
3409
- let zoneCount = taggingDetails.length;
3410
- let zoneName = taggingDetails.map( ( item ) => item.tagName );
3411
- let firstDate = dayjs( getStore[storeIndex]?.edge?.firstFileDate ).format( 'YYYY-MM-DD' );
3412
- let workingdays;
3413
- let workingdaystrax;
3414
- const givenDate = dayjs( requestData.date );
3415
- console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', getStore[storeIndex].storeId, trafficCameraCount, zoneCameraCount, zoneCount );
3416
- const isFirstDayOfMonth = givenDate.isSame( dayjs().startOf( 'month' ), 'day' );
3417
- if ( getStore[storeIndex]?.edge.firstFile ) {
3418
- if ( firstDate < requestData.date && getStore[storeIndex]?.status == 'active' &&
3419
- dailyData[0]?.dateString != dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) ) {
3420
- if ( dailyData.length && dailyData[0]?.stores?.daysDifference&&!isFirstDayOfMonth ) {
3421
- workingdays = dailyData[0]?.stores?.daysDifference + 1;
3422
- } else {
3423
- workingdays = 1;
3424
- }
3425
- } else {
3426
- if ( dailyData[0]?.stores?.daysDifference&&!isFirstDayOfMonth ) {
3427
- workingdays = dailyData[0]?.stores?.daysDifference;
3428
- } else {
3429
- workingdays = 0;
3430
- }
3431
- }
3432
- }
3433
- console.log( dailyData[0]?.stores );
3434
- if ( getStore[storeIndex]?.status == 'active' &&
3435
- dailyData[0]?.dateString != dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) ) {
3436
- if ( dailyData.length && dailyData[0]?.stores?.daysDifferenceTrax&&!isFirstDayOfMonth ) {
3437
- workingdaystrax = dailyData[0]?.stores?.daysDifferenceTrax + 1;
3438
- } else {
3439
- workingdaystrax = 1;
3440
- }
3441
- } else {
3442
- if ( dailyData[0]?.stores?.daysDifferenceTrax&&!isFirstDayOfMonth ) {
3443
- workingdaystrax = dailyData[0]?.stores?.daysDifferenceTrax;
3444
- } else {
3445
- workingdaystrax = 0;
3446
- }
3447
- }
3448
- let priceDetails = getClient.priceType == 'standard' ? getBaseprice.standard : getBaseprice.step;
3449
- priceDetails = priceDetails.filter( ( item1 ) =>
3450
- getClient.planDetails.product.some( ( item2 ) =>
3451
- item2.productName === item1.productName && item2.status === 'live',
3452
- ),
3453
- );
3454
- for ( let storeProductIndex = 0; storeProductIndex < getStore[storeIndex].product.length; storeProductIndex++ ) {
3455
- let productDetails;
3456
- if ( getClient.priceType == 'standard' ) {
3457
- productDetails = priceDetails.find( ( item ) => item.productName == getStore[storeIndex].product[storeProductIndex] );
3458
- } else {
3459
- productDetails = priceDetails.find( ( item ) => {
3460
- let range = item.storeRange.split( '-' );
3461
- if ( parseInt( range[0] ) <= ( storeIndex + 1 ) && parseInt( range[1] ) >= ( storeIndex + 1 ) ) {
3462
- return ( item.productName == getStore[storeIndex].product[storeProductIndex] && parseInt( range[0] ) <= ( storeIndex + 1 ) && parseInt( range[1] ) >= ( storeIndex + 1 ) );
3463
- }
3464
- } );
3465
- if ( !productDetails ) {
3466
- let stepProductDetails = priceDetails.filter( ( item ) => item.productName == getStore[storeIndex].product[storeProductIndex] );
3467
- productDetails = stepProductDetails[stepProductDetails.length - 1];
3468
- }
3469
- }
3470
- if ( productDetails ) {
3471
- console.log( '-------', workingdays, workingdaystrax, productDetails.productName );
3472
- let newObject = {
3473
- productName: productDetails.productName,
3474
- workingdays: productDetails.productName==='tangoTrax'?workingdaystrax:workingdays,
3475
- };
3476
- productList.push( newObject );
3477
- }
3478
- }
3479
- storeList.push(
3480
- {
3481
- storeId: getStore[storeIndex].storeId,
3482
- store: getStore[storeIndex]._id,
3483
- storeName: getStore[storeIndex].storeName,
3484
- status: getStore[storeIndex]?.status,
3485
- firstFile: getStore[storeIndex]?.edge?.firstFile || false,
3486
- edgefirstFileDate: getStore[storeIndex]?.edge?.firstFileDate || null,
3487
- date: new Date( requestData.date ),
3488
- daysDifference: workingdays,
3489
- daysDifferenceTrax: workingdaystrax,
3490
- products: productList,
3491
- camera: cameraDetails.map( ( item ) => item.streamName ),
3492
- trafficCameraCount: trafficCameraCount,
3493
- zoneCameraCount: zoneCameraCount,
3494
- zoneCount: zoneCount,
3495
- zoneName: zoneName,
3496
- },
3497
- );
3498
- console.log( storeIndex, getStore.length - 1 );
3499
- if ( storeIndex == getStore.length - 1 ) {
3500
- let activestores = storeList.filter( ( store ) => store.status=='active' );
3501
- console.log( '------', activestores.length );
3502
- let params = {
3503
- clientId: requestClient[clientIndex],
3504
- stores: storeList,
3505
- dateISO: new Date( requestData.date ),
3506
- accountType: getClient?.planDetails?.subscriptionType,
3507
- status: getClient?.status,
3508
- activeStores: activestores?.length,
3509
- brandName: getClient?.clientName,
3510
- proRate: getClient?.paymentInvoice?.proRate,
3511
- dateString: dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ),
3512
- };
3513
- await dailyPriceService.updateOne( { clientId: requestClient[clientIndex], dateString: params.dateString }, params, { upsert: true } );
3514
- }
3515
- // } ).catch( ( error ) => {
3516
- // logger.error( { error: error, function: 'old processedDayData' } );
3517
- // } );
3518
- } else {
3519
- // console.log( 'base price not found' );
3520
- }
3521
- }
3522
- } else {
3523
- // console.log( 'store not found' );
3524
- }
3525
- }
3526
- // console.log( clientIndex, requestClient.length-1 );
3527
- if ( clientIndex == requestClient.length - 1 ) {
3528
- // let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3529
- let totalcount = await dailyPriceService.find( { dateString: dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) } );
3530
- let teamsMsg = `${totalcount.length} clients data is inserted on ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3531
- console.log( teamsMsg );
3532
- // if ( teamsAlertUrls.invoiceAlert ) {
3533
- // sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3534
- // }
3535
3533
 
3536
- const SES = JSON.parse( process.env.SES );
3537
- let fromEmail = SES.accountsEmail;
3538
- console.log( process.env.invoiceAlert );
3539
- let invoiceEmails = JSON.parse( process.env.invoiceAlert );
3540
- let mailSubject = `Daily Invoice Alert ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3541
- if ( invoiceEmails ) {
3542
- const result = await sendEmailWithSES( invoiceEmails?.email, mailSubject, teamsMsg, '', fromEmail, [] );
3543
- console.log( '🚀 ~ dailyPricingInsert ~ result:', result );
3544
- }
3534
+ if ( requestClient.length > 0 ) {
3535
+ // Process clients in bounded batches so we run in parallel without
3536
+ // exhausting the DB connection pool with hundreds of concurrent clients.
3537
+ const BATCH_SIZE = 5;
3538
+ for ( let i = 0; i < requestClient.length; i += BATCH_SIZE ) {
3539
+ let batch = requestClient.slice( i, i + BATCH_SIZE );
3540
+ await Promise.all(
3541
+ batch.map( ( clientId ) =>
3542
+ processClientDailyPricing( clientId, requestData.date ).catch( ( error ) => {
3543
+ logger.error( { error: error, function: 'processClientDailyPricing', clientId: clientId } );
3544
+ } ),
3545
+ ),
3546
+ );
3547
+ }
3545
3548
 
3546
- return res.sendSuccess( 'Price Details Inserted Successfully' );
3547
- }
3549
+ // let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3550
+ let totalcount = await dailyPriceService.find( { dateString: dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) } );
3551
+ let teamsMsg = `${totalcount.length} clients data is inserted on ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3552
+ console.log( teamsMsg );
3553
+ // if ( teamsAlertUrls.invoiceAlert ) {
3554
+ // sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3555
+ // }
3556
+
3557
+ const SES = JSON.parse( process.env.SES );
3558
+ let fromEmail = SES.accountsEmail;
3559
+ console.log( process.env.invoiceAlert );
3560
+ let invoiceEmails = JSON.parse( process.env.invoiceAlert );
3561
+ let mailSubject = `Daily Invoice Alert ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3562
+ if ( invoiceEmails ) {
3563
+ const result = await sendEmailWithSES( invoiceEmails?.email, mailSubject, teamsMsg, '', fromEmail, [] );
3564
+ console.log( '🚀 ~ dailyPricingInsert ~ result:', result );
3548
3565
  }
3549
- return res.sendSuccess( 'success' );
3566
+
3567
+ return res.sendSuccess( 'Price Details Inserted Successfully' );
3550
3568
  }
3569
+ return res.sendSuccess( 'success' );
3551
3570
  } catch ( e ) {
3552
- logger.error( { error: e, function: 'invoiceCreate' } );
3571
+ logger.error( { error: e, function: 'dailyPricingInsert' } );
3553
3572
  return res.sendError( e, 500 );
3554
3573
  }
3555
3574
  };