tango-app-api-payment-subscription 3.5.4 → 3.5.5

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.4",
3
+ "version": "3.5.5",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "nodemon": "^3.1.0",
30
30
  "puppeteer": "^24.41.0",
31
31
  "swagger-ui-express": "^5.0.0",
32
- "tango-api-schema": "^2.5.77",
32
+ "tango-api-schema": "^2.6.9",
33
33
  "tango-app-api-middleware": "^3.6.18",
34
34
  "winston": "^3.12.0",
35
35
  "winston-daily-rotate-file": "^5.0.0",
@@ -0,0 +1,66 @@
1
+ // One-shot migration: replaces the legacy billing-group proRata values
2
+ // 'before15' → 'flat' and 'after15' → 'prorate'. Idempotent — safe to
3
+ // re-run; rows already on the new values are skipped.
4
+ //
5
+ // Usage:
6
+ // MONGO_URI="mongodb://..." node scripts/migrate-billing-prorata-pricing.js
7
+ //
8
+ // Sequencing: MUST run BEFORE deploying the tightened schema enum
9
+ // ['prorate','flat'] in tango-api-schema, otherwise updateBillingGroup
10
+ // calls on unmigrated rows will fail Mongoose ValidationError.
11
+
12
+ import mongoose from 'mongoose';
13
+ import 'dotenv/config';
14
+
15
+ const NEW_VALUES = [ 'prorate', 'flat' ];
16
+
17
+ async function run() {
18
+ const uri = process.env.MONGO_URI;
19
+ if ( !uri ) {
20
+ console.error( 'MONGO_URI env var is required' );
21
+ process.exit( 1 );
22
+ }
23
+
24
+ await mongoose.connect( uri );
25
+ // strict:false so we don't pin to a specific tango-api-schema version.
26
+ const Billing = mongoose.model(
27
+ '_migrateBilling',
28
+ new mongoose.Schema( {}, { strict: false } ),
29
+ 'billings',
30
+ );
31
+
32
+ // before15 (legacy "full-month for stores active most of the month") → flat
33
+ const flatResult = await Billing.updateMany(
34
+ { proRata: 'before15' },
35
+ { $set: { proRata: 'flat' } },
36
+ );
37
+ console.log( `Migrated ${flatResult.modifiedCount} billing group(s) from 'before15' → 'flat'` );
38
+
39
+ // after15 (legacy "always actual working days") → prorate
40
+ const prorateResult = await Billing.updateMany(
41
+ { proRata: 'after15' },
42
+ { $set: { proRata: 'prorate' } },
43
+ );
44
+ console.log( `Migrated ${prorateResult.modifiedCount} billing group(s) from 'after15' → 'prorate'` );
45
+
46
+ // Surface any rows whose proRata is something unexpected (manual review).
47
+ const stray = await Billing.find(
48
+ { proRata: { $exists: true, $nin: [ ...NEW_VALUES, null, '' ] } },
49
+ { _id: 1, clientId: 1, groupName: 1, proRata: 1 },
50
+ ).limit( 50 ).lean();
51
+
52
+ if ( stray.length ) {
53
+ console.warn( `Found ${stray.length} row(s) with unexpected proRata values:` );
54
+ stray.forEach( ( s ) => console.warn( ' -', s._id.toString(), s.clientId, s.groupName, '=>', s.proRata ) );
55
+ console.warn( 'Review these manually before tightening the schema enum.' );
56
+ } else {
57
+ console.log( 'No stray proRata values found. Safe to tighten the schema.' );
58
+ }
59
+
60
+ await mongoose.disconnect();
61
+ }
62
+
63
+ run().catch( ( err ) => {
64
+ console.error( err );
65
+ process.exit( 1 );
66
+ } );
@@ -4,6 +4,7 @@ import * as invoice from '../services/invoice.service.js';
4
4
  import { aggregatebilling, countDocuments, create, deleteOne, find, findOne, updateMany, updateOne } from '../services/billing.service.js';
5
5
  import * as invoiceService from '../services/invoice.service.js';
6
6
  import mongoose from 'mongoose';
7
+ import axios from 'axios';
7
8
  import dayjs from 'dayjs';
8
9
  import { leadGet } from '../services/lead.service.js';
9
10
  import { findOneClient } from '../services/clientPayment.services.js';
@@ -577,7 +578,6 @@ export const getInvoices = async ( req, res ) => {
577
578
  filterStartDate = new Date( dayjs().subtract( 3, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
578
579
  filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
579
580
  }
580
-
581
581
  if ( req.body?.filter && !req.body?.searchValue ) {
582
582
  matchStage.$match['$and'] = [
583
583
  { billingDate: { $gte: filterStartDate } },
@@ -868,4 +868,145 @@ export async function getClientProducts( req, res ) {
868
868
  }
869
869
  }
870
870
 
871
+ // GSTIN address lookup via Sheet GST Check
872
+ // (https://sheet.gstincheck.co.in). Returns the registered company name
873
+ // + principal place of business address so the UI can auto-fill the
874
+ // billing-group form.
875
+ //
876
+ // Requires GST_LOOKUP_API_KEY in the API environment. The key is embedded
877
+ // in the provider's URL path (their auth scheme); we never log it.
878
+ //
879
+ // Caller validates GSTIN format (15 chars, alphanumeric). Server-side we
880
+ // re-check before forwarding to the provider.
881
+ export async function gstinLookup( req, res ) {
882
+ try {
883
+ const gstin = String( req.params.gstin || '' ).trim().toUpperCase();
884
+ if ( !/^[0-9A-Z]{15}$/.test( gstin ) ) {
885
+ return res.sendError( 'Invalid GSTIN format', 400 );
886
+ }
887
+
888
+ // GSTIN state-code prefix → State + Place of Supply (matches the
889
+ // table the frontend already maintains).
890
+ const stateMap = {
891
+ '01': 'Jammu and Kashmir', '02': 'Himachal Pradesh', '03': 'Punjab',
892
+ '04': 'Chandigarh', '05': 'Uttarakhand', '06': 'Haryana', '07': 'Delhi',
893
+ '08': 'Rajasthan', '09': 'Uttar Pradesh', '10': 'Bihar', '11': 'Sikkim',
894
+ '12': 'Arunachal Pradesh', '13': 'Nagaland', '14': 'Manipur',
895
+ '15': 'Mizoram', '16': 'Tripura', '17': 'Meghalaya', '18': 'Assam',
896
+ '19': 'West Bengal', '20': 'Jharkhand', '21': 'Odisha',
897
+ '22': 'Chhattisgarh', '23': 'Madhya Pradesh', '24': 'Gujarat',
898
+ '25': 'Daman and Diu', '27': 'Maharashtra', '29': 'Karnataka',
899
+ '30': 'Goa', '32': 'Kerala', '33': 'Tamil Nadu', '34': 'Puducherry',
900
+ '36': 'Telangana', '37': 'Andhra Pradesh',
901
+ };
902
+ const code = gstin.substring( 0, 2 );
903
+ const stateName = stateMap[code];
904
+ if ( !stateName ) {
905
+ return res.sendError( 'Unknown state code in GSTIN', 404 );
906
+ }
907
+
908
+ // Real lookup via Sheet GST Check (https://gstincheck.co.in/).
909
+ // Endpoint: GET https://sheet.gstincheck.co.in/check/{API_KEY}/{GSTIN}
910
+ // Auth: API key embedded in the URL path (no header). Set
911
+ // GST_LOOKUP_API_KEY in the API's environment.
912
+ const apiKey = process.env.GST_LOOKUP_API_KEY;
913
+ console.log( '🚀 ~ gstinLookup ~ apiKey:', apiKey );
914
+ if ( !apiKey ) {
915
+ // Fail explicitly rather than silently fall back — that way a missing
916
+ // key shows up immediately instead of mock data leaking into prod.
917
+ logger.error( { function: 'gstinLookup', message: 'GST_LOOKUP_API_KEY not set' } );
918
+ return res.sendError( 'GST lookup is not configured on the server', 500 );
919
+ }
920
+
921
+ let providerResponse;
922
+ try {
923
+ providerResponse = await axios.get(
924
+ `https://sheet.gstincheck.co.in/check/${encodeURIComponent( apiKey )}/${encodeURIComponent( gstin )}`,
925
+ { timeout: 8000 },
926
+ );
927
+ } catch ( axiosErr ) {
928
+ // Distinguish provider-down vs other errors so the client can show a
929
+ // useful message. 4xx from the provider is treated as a real error;
930
+ // network errors fall through to a generic 502.
931
+ const status = axiosErr?.response?.status;
932
+ logger.error( { error: axiosErr?.message, status, function: 'gstinLookup.provider', gstin } );
933
+ if ( status === 404 ) {
934
+ return res.sendError( 'GSTIN not found', 404 );
935
+ }
936
+ return res.sendError( 'GST lookup service unavailable', 502 );
937
+ }
938
+
939
+ const body = providerResponse?.data || {};
940
+ if ( !body.flag || !body.data ) {
941
+ // Provider reached but couldn't resolve this GSTIN.
942
+ return res.sendError( 'GSTIN not found', 404 );
943
+ }
944
+
945
+ // Map provider's response shape into the contract the frontend expects.
946
+ // Provider uses Indian-government short names (lgnm, tradeNam, pradr,
947
+ // sts, etc.); we normalize to our flat structure.
948
+ const src = body.data;
949
+ // TEMP: full raw response logged so we can confirm field names per
950
+ // GSTIN — remove once the mapping is verified across a few real cases.
951
+ logger.error( { function: 'gstinLookup.raw', gstin, providerData: src } );
952
+
953
+ const principal = src.pradr || src.pradr1 || {};
954
+ // The provider has used both `addr` (newer) and a flatter form
955
+ // (older). Coalesce — `addr` if present, otherwise the principal
956
+ // itself contains the address fields.
957
+ const addr = principal.addr || principal || {};
958
+
959
+ // If the provider returned a fully-formed display address string, use
960
+ // it directly for addressLineOne — that's the canonical comma-joined
961
+ // address GSTN itself displays. Otherwise build it from the
962
+ // individual fields. This avoids re-joining in a different order than
963
+ // the registry intended.
964
+ const fullAddr = ( principal.adr || principal.address || src.adr || '' ).trim();
965
+
966
+ let addressLineOne;
967
+ let addressLineTwo;
968
+ if ( fullAddr ) {
969
+ // The registry format is `bno, bnm, flno, st, loc, lndmrk, city,
970
+ // dst, stcd, pncd`. Split on commas, take the first half for line 1
971
+ // and the rest (minus city/state/pincode tail which goes to the
972
+ // separate fields) for line 2. Heuristic: split into two halves.
973
+ const parts = fullAddr.split( ',' ).map( ( s ) => s.trim() ).filter( Boolean );
974
+ const half = Math.ceil( parts.length / 2 );
975
+ addressLineOne = parts.slice( 0, half ).join( ', ' );
976
+ addressLineTwo = parts.slice( half ).join( ', ' );
977
+ } else {
978
+ const line1Parts = [ addr.bno, addr.bnm, addr.flno, addr.st ].filter( ( p ) => p && String( p ).trim() );
979
+ const line2Parts = [ addr.loc, addr.lndmrk, addr.dst ].filter( ( p ) => p && String( p ).trim() );
980
+ addressLineOne = line1Parts.join( ', ' );
981
+ addressLineTwo = line2Parts.join( ', ' );
982
+ }
983
+
984
+ // City: prefer explicit `city` / `dst`. Some records put the city
985
+ // name in `loc` and the colony / sub-area in `dst` — prefer city, but
986
+ // fall back to dst then loc when missing.
987
+ const cityValue = addr.city || addr.dst || addr.loc || '';
988
+
989
+ const data = {
990
+ gstin: src.gstin || gstin,
991
+ legalName: src.lgnm || src.legalName || '',
992
+ tradeName: src.tradeNam || src.tradeName || '',
993
+ addressLineOne,
994
+ addressLineTwo,
995
+ city: cityValue,
996
+ // Prefer provider's state name; fall back to our state-code map so a
997
+ // missing field doesn't blank out the Place of Supply.
998
+ state: addr.stcd || addr.state || stateName,
999
+ country: 'India',
1000
+ pinCode: addr.pncd ? String( addr.pncd ) : ( addr.pincode ? String( addr.pincode ) : '' ),
1001
+ placeOfSupply: `${addr.stcd || addr.state || stateName} (${code})`,
1002
+ status: src.sts || '',
1003
+ };
1004
+
1005
+ return res.sendSuccess( data );
1006
+ } catch ( error ) {
1007
+ logger.error( { error: error, function: 'gstinLookup' } );
1008
+ return res.sendError( error, 500 );
1009
+ }
1010
+ }
1011
+
871
1012
 
@@ -339,6 +339,7 @@ export async function brandInvoiceList( req, res ) {
339
339
  totalAmount: 1,
340
340
  status: 1,
341
341
  paymentStatus: 1,
342
+ paidAmount: { $ifNull: [ '$paidAmount', 0 ] },
342
343
  clientId: 1,
343
344
  currency: 1,
344
345
  dueDate: 1,
@@ -208,7 +208,14 @@ export async function createInvoice( req, res ) {
208
208
  let amount = products.reduce( ( sum, product ) => sum + product.amount, 0 );
209
209
  let taxList = [];
210
210
  let totalAmount = 0;
211
- if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
211
+ // International billing groups: skip tax calculation entirely. The
212
+ // tax array stays empty so the PDF won't render a tax line and
213
+ // totalAmount equals subtotal. Default 'domestic' preserves the
214
+ // existing GST/IGST/CGST/SGST behavior for legacy records that have
215
+ // no taxCalculationType set yet.
216
+ if ( group.taxCalculationType === 'international' ) {
217
+ totalAmount = Math.round( amount );
218
+ } else if ( group.gst && group.gst.slice( 0, 2 ) == '33' ) {
212
219
  let taxAmount = ( amount * 18 ) / 100;
213
220
  totalAmount = Math.round( amount + taxAmount );
214
221
  taxList.push(
@@ -369,7 +376,7 @@ export async function invoiceDownload( req, res ) {
369
376
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
370
377
  firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
371
378
  item.productName = firstWord + ' ' + secondWord;
372
- item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
379
+ item.price = item.price .toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
373
380
  item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
374
381
  item.currency = invoiceCurrency;
375
382
  } );
@@ -1005,6 +1012,11 @@ function inWords( num ) {
1005
1012
  async function standardPrice( group, getClient, baseDate ) {
1006
1013
  console.log( '🚀 ~ standardPrice ~ baseDate:', baseDate.format( 'MMM YYYY' ) );
1007
1014
  const currentMonthDays = dayjs().daysInMonth();
1015
+ // Pricing method: 'flat' => bill every store for the full month
1016
+ // regardless of working days. 'prorate' => bill for actual working days.
1017
+ // Computed once so the aggregation pipelines can inline a $literal.
1018
+ const isFlatPricing = group.proRata === 'flat';
1019
+ console.log( '🚀 ~ standardPrice ~ isFlatPricing:', isFlatPricing );
1008
1020
  let billingTypeMap = {};
1009
1021
  if ( getClient?.planDetails?.product ) {
1010
1022
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1052,6 +1064,13 @@ async function standardPrice( group, getClient, baseDate ) {
1052
1064
  storeStatus: '$stores.status',
1053
1065
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1054
1066
  cameraCount: { $ifNull: [ '$stores.cameraCount', 0 ] },
1067
+ // Pull per-store camera splits off the daily-pricing store doc so the
1068
+ // downstream perCamera branch (see below) can multiply price by the
1069
+ // actual camera count. Without these the second $group would $sum
1070
+ // missing fields and totalTraffic/ZoneCameraCount would always be 0
1071
+ // — silently degrading perCamera billing to perStore.
1072
+ trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1073
+ zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1055
1074
  },
1056
1075
  },
1057
1076
  {
@@ -1063,23 +1082,31 @@ async function standardPrice( group, getClient, baseDate ) {
1063
1082
  $project: {
1064
1083
  productName: 1,
1065
1084
  storeId: 1,
1066
- prorate: { $literal: group.proRata },
1085
+ // isFlatPricing is the group-level pricing flag, baked into every
1086
+ // doc so the next $project's $cond can read it.
1087
+ isFlatPricing: { $literal: isFlatPricing },
1067
1088
  workingDays: 1,
1068
1089
  storeStatus: 1,
1069
1090
  zoneCount: 1,
1070
1091
  cameraCount: 1,
1092
+ trafficCameraCount: 1,
1093
+ zoneCameraCount: 1,
1071
1094
  },
1072
1095
  },
1073
1096
  {
1074
1097
  $project: {
1075
1098
  productName: 1,
1076
1099
  storeId: 1,
1100
+ // Flat pricing => every store billed for the full month.
1101
+ // Prorate => keep the actual workingDays.
1077
1102
  workingDays: {
1078
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1103
+ $cond: { if: '$isFlatPricing', then: currentMonthDays, else: '$workingDays' },
1079
1104
  },
1080
1105
  storeStatus: 1,
1081
1106
  zoneCount: 1,
1082
1107
  cameraCount: 1,
1108
+ trafficCameraCount: 1,
1109
+ zoneCameraCount: 1,
1083
1110
  },
1084
1111
  },
1085
1112
  {
@@ -1091,6 +1118,8 @@ async function standardPrice( group, getClient, baseDate ) {
1091
1118
  workingdays: { $first: '$workingDays' },
1092
1119
  zoneCount: { $first: '$zoneCount' },
1093
1120
  cameraCount: { $first: '$cameraCount' },
1121
+ trafficCameraCount: { $first: '$trafficCameraCount' },
1122
+ zoneCameraCount: { $first: '$zoneCameraCount' },
1094
1123
  },
1095
1124
  },
1096
1125
  {
@@ -1294,16 +1323,24 @@ async function standardPrice( group, getClient, baseDate ) {
1294
1323
  storeStatus: '$stores.status',
1295
1324
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1296
1325
  cameraCount: { $ifNull: [ '$stores.cameraCount', 0 ] },
1326
+ // perCamera (eachStore branch) reads these on the per-store record
1327
+ // below; projecting them through is required for the camera-count
1328
+ // multiplier to fire.
1329
+ trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1330
+ zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1297
1331
  },
1298
1332
  },
1299
1333
  { $match: { workingDays: { $gt: 0 }, productName: { $in: eachStoreProductNames } } },
1300
1334
  {
1301
1335
  $project: {
1302
1336
  productName: 1, storeId: 1, storeName: 1,
1337
+ // Flat pricing => full month per store. Prorate => actual working
1338
+ // days. Same rule as the overall-store branch above.
1303
1339
  workingDays: {
1304
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ { $literal: group.proRata }, 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1340
+ $cond: { if: { $literal: isFlatPricing }, then: currentMonthDays, else: '$workingDays' },
1305
1341
  },
1306
1342
  zoneCount: 1, cameraCount: 1,
1343
+ trafficCameraCount: 1, zoneCameraCount: 1,
1307
1344
  },
1308
1345
  },
1309
1346
  {
@@ -1331,6 +1368,11 @@ async function standardPrice( group, getClient, baseDate ) {
1331
1368
  {
1332
1369
  $project: {
1333
1370
  productName: 1, storeId: 1, storeName: 1, workingDays: 1, zoneCount: 1, cameraCount: 1,
1371
+ // Final whitelist project — must carry trafficCameraCount and
1372
+ // zoneCameraCount through to the JS consumer. Without these the
1373
+ // perCamera branch in the for-loop below sees undefined and falls
1374
+ // back to storeCount=1.
1375
+ trafficCameraCount: 1, zoneCameraCount: 1,
1334
1376
  price: '$matchedStandard.negotiatePrice',
1335
1377
  period: { $cond: { if: { $lt: [ '$workingDays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
1336
1378
  },
@@ -1339,6 +1381,7 @@ async function standardPrice( group, getClient, baseDate ) {
1339
1381
 
1340
1382
  for ( let store of perStoreData ) {
1341
1383
  let productBillingType = billingTypeMap[store.productName] || 'perStore';
1384
+
1342
1385
  let storeCount = 1;
1343
1386
  if ( store.productName === 'tangoZone' ) {
1344
1387
  if ( productBillingType === 'perZone' && store.zoneCount > 0 ) {
@@ -1406,6 +1449,9 @@ async function standardPrice( group, getClient, baseDate ) {
1406
1449
 
1407
1450
  async function stepPrice( group, getClient ) {
1408
1451
  const currentMonthDays = dayjs().daysInMonth();
1452
+ // 'flat' => every store billed for full month.
1453
+ // 'prorate' => actual working days. See standardPrice for the same flag.
1454
+ const isFlatPricing = group.proRata === 'flat';
1409
1455
  let billingTypeMap = {};
1410
1456
  if ( getClient?.planDetails?.product ) {
1411
1457
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1453,6 +1499,11 @@ async function stepPrice( group, getClient ) {
1453
1499
  storeStatus: '$stores.status',
1454
1500
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1455
1501
  cameraCount: { $ifNull: [ '$stores.cameraCount', 0 ] },
1502
+ // Pull per-store camera splits; same fix as standardPrice — without
1503
+ // these the second $group sums missing fields and the perCamera
1504
+ // branch in the downstream map silently falls back to perStore.
1505
+ trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1506
+ zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1456
1507
  },
1457
1508
  },
1458
1509
  {
@@ -1464,23 +1515,30 @@ async function stepPrice( group, getClient ) {
1464
1515
  $project: {
1465
1516
  productName: 1,
1466
1517
  storeId: 1,
1467
- prorate: { $literal: group.proRata },
1518
+ // Group-level flag baked into every doc; consumed by the next
1519
+ // $project's $cond.
1520
+ isFlatPricing: { $literal: isFlatPricing },
1468
1521
  workingDays: 1,
1469
1522
  storeStatus: 1,
1470
1523
  zoneCount: 1,
1471
1524
  cameraCount: 1,
1525
+ trafficCameraCount: 1,
1526
+ zoneCameraCount: 1,
1472
1527
  },
1473
1528
  },
1474
1529
  {
1475
1530
  $project: {
1476
1531
  productName: 1,
1477
1532
  storeId: 1,
1533
+ // Flat => full month per store. Prorate => actual working days.
1478
1534
  workingDays: {
1479
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1535
+ $cond: { if: '$isFlatPricing', then: currentMonthDays, else: '$workingDays' },
1480
1536
  },
1481
1537
  storeStatus: 1,
1482
1538
  zoneCount: 1,
1483
1539
  cameraCount: 1,
1540
+ trafficCameraCount: 1,
1541
+ zoneCameraCount: 1,
1484
1542
  },
1485
1543
  }, {
1486
1544
  $group: {
@@ -1491,6 +1549,8 @@ async function stepPrice( group, getClient ) {
1491
1549
  workingdays: { $first: '$workingDays' },
1492
1550
  zoneCount: { $first: '$zoneCount' },
1493
1551
  cameraCount: { $first: '$cameraCount' },
1552
+ trafficCameraCount: { $first: '$trafficCameraCount' },
1553
+ zoneCameraCount: { $first: '$zoneCameraCount' },
1494
1554
  },
1495
1555
  },
1496
1556
  {
@@ -1559,16 +1619,24 @@ async function stepPrice( group, getClient ) {
1559
1619
  storeStatus: '$stores.status',
1560
1620
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1561
1621
  cameraCount: { $ifNull: [ '$stores.cameraCount', 0 ] },
1622
+ // perCamera (eachStore branch) reads these on the per-store record
1623
+ // below; projecting them through is required for the camera-count
1624
+ // multiplier to fire.
1625
+ trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1626
+ zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1562
1627
  },
1563
1628
  },
1564
1629
  { $match: { workingDays: { $gt: 0 }, productName: { $in: eachStoreProductNames } } },
1565
1630
  {
1566
1631
  $project: {
1567
1632
  productName: 1, storeId: 1, storeName: 1,
1633
+ // Flat pricing => full month per store. Prorate => actual working
1634
+ // days. Same rule as the overall-store branch above.
1568
1635
  workingDays: {
1569
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ { $literal: group.proRata }, 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1636
+ $cond: { if: { $literal: isFlatPricing }, then: currentMonthDays, else: '$workingDays' },
1570
1637
  },
1571
1638
  zoneCount: 1, cameraCount: 1,
1639
+ trafficCameraCount: 1, zoneCameraCount: 1,
1572
1640
  },
1573
1641
  },
1574
1642
  {
@@ -1770,35 +1838,64 @@ export async function clientInvoiceList( req, res ) {
1770
1838
  clientId: { $in: findClients },
1771
1839
  },
1772
1840
  } ];
1841
+
1842
+ // If the user picked an explicit Month or Year, ignore the Duration
1843
+ // filter — otherwise the two ranges conflict (e.g. "current month" +
1844
+ // April would always return zero results). Mirrors brand-invoices.
1845
+ const hasMonthYear = ( req.body.monthFilter && String( req.body.monthFilter ) !== '' ) ||
1846
+ ( req.body.yearFilter && String( req.body.yearFilter ) !== '' );
1847
+
1773
1848
  let filterStartDate = '';
1774
1849
  let filterEndDate = '';
1775
1850
 
1776
- if ( req.body?.filter && req.body.filter == 'current' ) {
1777
- filterStartDate = new Date( dayjs().startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1778
- filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1779
- }
1780
- if ( req.body?.filter && req.body.filter == 'prev' ) {
1781
- filterStartDate = new Date( dayjs().subtract( 1, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1782
- filterEndDate = new Date( dayjs().subtract( 1, 'month' ).endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1783
- }
1784
- if ( req.body?.filter && req.body.filter == 'last' ) {
1785
- filterStartDate = new Date( dayjs().subtract( 12, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1786
- filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1787
- }
1788
- console.log( filterStartDate, filterEndDate );
1851
+ if ( !hasMonthYear ) {
1852
+ if ( req.body?.filter && req.body.filter == 'current' ) {
1853
+ filterStartDate = new Date( dayjs().startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1854
+ filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1855
+ }
1856
+ if ( req.body?.filter && req.body.filter == 'prev' ) {
1857
+ filterStartDate = new Date( dayjs().subtract( 1, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1858
+ filterEndDate = new Date( dayjs().subtract( 1, 'month' ).endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1859
+ }
1860
+ if ( req.body?.filter && req.body.filter == 'last' ) {
1861
+ filterStartDate = new Date( dayjs().subtract( 12, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1862
+ filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1863
+ }
1789
1864
 
1790
- if ( req.body?.filter ) {
1791
- query.push(
1792
- {
1793
- $match: {
1794
- $and: [
1795
- { billingDate: { $gte: filterStartDate } },
1796
- { billingDate: { $lte: filterEndDate } },
1797
- ],
1798
- },
1865
+ if ( req.body?.filter ) {
1866
+ query.push( {
1867
+ $match: {
1868
+ $and: [
1869
+ { billingDate: { $gte: filterStartDate } },
1870
+ { billingDate: { $lte: filterEndDate } },
1871
+ ],
1799
1872
  },
1873
+ } );
1874
+ }
1875
+ }
1800
1876
 
1801
- );
1877
+ // Month / Year filters (independent of durationFilter). Both 1-based:
1878
+ // monthFilter '1'..'12', yearFilter four-digit string like '2026'.
1879
+ // billingDate may be stored as Date OR string in some legacy rows, so
1880
+ // coerce inside the pipeline before $year/$month.
1881
+ const monthNum = req.body.monthFilter ? parseInt( req.body.monthFilter, 10 ) : null;
1882
+ const yearNum = req.body.yearFilter ? parseInt( req.body.yearFilter, 10 ) : null;
1883
+ if ( ( monthNum && monthNum >= 1 && monthNum <= 12 ) || yearNum ) {
1884
+ const billingDateExpr = {
1885
+ $cond: [
1886
+ { $eq: [ { $type: '$billingDate' }, 'date' ] },
1887
+ '$billingDate',
1888
+ { $toDate: '$billingDate' },
1889
+ ],
1890
+ };
1891
+ const expr = { $and: [] };
1892
+ if ( yearNum ) {
1893
+ expr.$and.push( { $eq: [ { $year: billingDateExpr }, yearNum ] } );
1894
+ }
1895
+ if ( monthNum && monthNum >= 1 && monthNum <= 12 ) {
1896
+ expr.$and.push( { $eq: [ { $month: billingDateExpr }, monthNum ] } );
1897
+ }
1898
+ query.push( { $match: { $expr: expr } } );
1802
1899
  }
1803
1900
 
1804
1901
 
@@ -1835,6 +1932,7 @@ export async function clientInvoiceList( req, res ) {
1835
1932
  clientName: '$clientDetails.clientName',
1836
1933
  logo: '$clientDetails.logo',
1837
1934
  currencyType: '$currency',
1935
+ currency: 1,
1838
1936
  invoice: 1,
1839
1937
  stores: 1,
1840
1938
  amount: 1,
@@ -1842,6 +1940,7 @@ export async function clientInvoiceList( req, res ) {
1842
1940
  groupName: 1,
1843
1941
  status: 1,
1844
1942
  paymentStatus: 1,
1943
+ paidAmount: { $ifNull: [ '$paidAmount', 0 ] },
1845
1944
  clientId: 1,
1846
1945
  billingDate: 1,
1847
1946
  },
@@ -2147,6 +2246,105 @@ export async function PaymentStatusChange( req, res ) {
2147
2246
  }
2148
2247
  }
2149
2248
 
2249
+ // Record a (full or partial) payment against an invoice. Appends to
2250
+ // paymentHistory, increments paidAmount, and recomputes paymentStatus
2251
+ // (unpaid → partial → paid). Body: { invoiceId, amount, date, method?,
2252
+ // reference?, notes? }. invoiceId is the human-readable invoice number
2253
+ // (e.g. INV-26-27-00077), matching the legacy PaymentStatusChange contract.
2254
+ export async function recordPayment( req, res ) {
2255
+ try {
2256
+ const { invoiceId, amount, date, method, reference, notes } = req.body;
2257
+ if ( !invoiceId ) {
2258
+ return res.sendError( 'invoiceId is required', 400 );
2259
+ }
2260
+ const amountNum = Number( amount );
2261
+ if ( !Number.isFinite( amountNum ) || amountNum <= 0 ) {
2262
+ return res.sendError( 'amount must be a positive number', 400 );
2263
+ }
2264
+
2265
+ const invoice = await invoiceService.findOne( { invoice: invoiceId } );
2266
+ if ( !invoice ) {
2267
+ return res.sendError( 'Invoice not found', 404 );
2268
+ }
2269
+
2270
+ const previousPaid = Number( invoice.paidAmount ) || 0;
2271
+ const newPaid = Math.round( ( previousPaid + amountNum ) * 100 ) / 100;
2272
+ const totalAmount = Number( invoice.totalAmount ) || 0;
2273
+
2274
+ // Reject overpayment — finance teams want this caught early. They can
2275
+ // record an exact final payment that brings the running total to the
2276
+ // invoice total; anything beyond that is a data-entry error.
2277
+ if ( totalAmount > 0 && newPaid > totalAmount + 0.01 ) {
2278
+ return res.sendError(
2279
+ `Payment exceeds outstanding balance. Outstanding: ${( totalAmount - previousPaid ).toFixed( 2 )}`,
2280
+ 400,
2281
+ );
2282
+ }
2283
+
2284
+ let derivedStatus = 'unpaid';
2285
+ if ( newPaid >= totalAmount - 0.01 && totalAmount > 0 ) {
2286
+ derivedStatus = 'paid';
2287
+ } else if ( newPaid > 0 ) {
2288
+ derivedStatus = 'partial';
2289
+ }
2290
+
2291
+ const historyEntry = {
2292
+ amount: amountNum,
2293
+ date: date ? new Date( date ) : new Date(),
2294
+ method: method || undefined,
2295
+ reference: reference || undefined,
2296
+ notes: notes || undefined,
2297
+ recordedBy: req.user?.email || req.user?.userName || undefined,
2298
+ recordedAt: new Date(),
2299
+ };
2300
+
2301
+ const update = {
2302
+ $set: {
2303
+ paidAmount: newPaid,
2304
+ paymentStatus: derivedStatus,
2305
+ ...( derivedStatus === 'paid' ? { paidDate: new Date() } : {} ),
2306
+ },
2307
+ $push: { paymentHistory: historyEntry },
2308
+ };
2309
+
2310
+ // Use invoiceUpdateOne — it passes the raw update object straight to
2311
+ // Mongoose. invoiceService.updateOne (without "invoice" prefix) wraps
2312
+ // its second arg in $set, which would turn our { $set, $push } into
2313
+ // { $set: { $set, $push } } and Mongoose silently drops both as
2314
+ // unknown fields under strict mode. (That's why an earlier version
2315
+ // of this controller responded 200 but didn't actually persist.)
2316
+ const result = await invoiceService.invoiceUpdateOne( { invoice: invoiceId }, update );
2317
+ logger.info?.( { function: 'recordPayment', invoiceId, matched: result?.matchedCount, modified: result?.modifiedCount } );
2318
+
2319
+ try {
2320
+ const logObj = {
2321
+ userName: req.user?.userName,
2322
+ email: req.user?.email,
2323
+ clientId: invoice.clientId,
2324
+ logSubType: 'paymentRecorded',
2325
+ logType: 'invoice',
2326
+ date: new Date(),
2327
+ changes: [ `Payment of ${amountNum} recorded for ${invoiceId}. Status: ${derivedStatus} (${newPaid}/${totalAmount}).` ],
2328
+ eventType: 'update',
2329
+ timestamp: new Date(),
2330
+ showTo: [ 'tango' ],
2331
+ };
2332
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
2333
+ } catch ( logErr ) {
2334
+ logger.error( { error: logErr, function: 'recordPayment.log' } );
2335
+ }
2336
+
2337
+ return res.sendSuccess( {
2338
+ paidAmount: newPaid,
2339
+ pendingAmount: Math.max( 0, totalAmount - newPaid ),
2340
+ paymentStatus: derivedStatus,
2341
+ } );
2342
+ } catch ( error ) {
2343
+ logger.error( { error: error, function: 'recordPayment' } );
2344
+ return res.sendError( error, 500 );
2345
+ }
2346
+ }
2347
+
2150
2348
  export async function checkPaymentStatus( req, res ) {
2151
2349
  try {
2152
2350
  let findInvoice = await invoiceService.find( { status: 'approved', paymentStatus: { $ne: 'paid' } } );
@@ -1,6 +1,6 @@
1
1
 
2
2
  /* eslint-disable new-cap */
3
- import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById, sendTeamsNotification } from 'tango-app-api-middleware';
3
+ import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById } from 'tango-app-api-middleware';
4
4
  import * as paymentService from '../services/clientPayment.services.js';
5
5
  import * as basePriceService from '../services/basePrice.service.js';
6
6
  import * as storeService from '../services/store.service.js';
@@ -3332,7 +3332,7 @@ export const dailyPricingInsert = async ( req, res ) => {
3332
3332
  ];
3333
3333
  let dailyData = await dailyPriceService.aggregate( query );
3334
3334
  let cameraDetails = await cameraService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], isActivated: true, isUp: true }, { streamName: 1, productModule: 1 } );
3335
- console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3335
+ // console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3336
3336
 
3337
3337
  let trafficCameraCount = cameraDetails.filter( ( cam ) =>
3338
3338
  ( cam.productModule || [] ).some( ( mod ) =>
@@ -3351,14 +3351,16 @@ export const dailyPricingInsert = async ( req, res ) => {
3351
3351
  mod.productName === 'tangoZone' && mod.checked === true,
3352
3352
  ),
3353
3353
  ).map( ( cam ) => cam.streamName );
3354
- let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: zoneCameraStreamNames } }, { tagName: 1 } );
3354
+ let allcameraname = cameraDetails?.map( ( cam ) => cam?.streamName );
3355
+ console.log( '🚀 ~ dailyPricingInsert ~ zoneCameraStreamNames:', zoneCameraStreamNames );
3356
+ let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: allcameraname } }, { tagName: 1 } );
3355
3357
  let zoneCount = taggingDetails.length;
3356
3358
  let zoneName = taggingDetails.map( ( item ) => item.tagName );
3357
3359
  let firstDate = dayjs( getStore[storeIndex]?.edge?.firstFileDate ).format( 'YYYY-MM-DD' );
3358
3360
  let workingdays;
3359
3361
  let workingdaystrax;
3360
3362
  const givenDate = dayjs( requestData.date );
3361
- console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', trafficCameraCount, zoneCameraCount, zoneCount );
3363
+ console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', getStore[storeIndex].storeId, trafficCameraCount, zoneCameraCount, zoneCount );
3362
3364
  const isFirstDayOfMonth = givenDate.isSame( dayjs().startOf( 'month' ), 'day' );
3363
3365
  if ( getStore[storeIndex]?.edge.firstFile ) {
3364
3366
  if ( firstDate < requestData.date && getStore[storeIndex]?.status == 'active' &&
@@ -3471,13 +3473,24 @@ export const dailyPricingInsert = async ( req, res ) => {
3471
3473
  }
3472
3474
  // console.log( clientIndex, requestClient.length-1 );
3473
3475
  if ( clientIndex == requestClient.length - 1 ) {
3474
- let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3476
+ // let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3475
3477
  let totalcount = await dailyPriceService.find( { dateString: dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) } );
3476
3478
  let teamsMsg = `${totalcount.length} clients data is inserted on ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3477
3479
  console.log( teamsMsg );
3478
- if ( teamsAlertUrls.invoiceAlert ) {
3479
- sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3480
+ // if ( teamsAlertUrls.invoiceAlert ) {
3481
+ // sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3482
+ // }
3483
+
3484
+ const SES = JSON.parse( process.env.SES );
3485
+ let fromEmail = SES.accountsEmail;
3486
+ console.log( process.env.invoiceAlert );
3487
+ let invoiceEmails = JSON.parse( process.env.invoiceAlert );
3488
+ let mailSubject = `Daily Invoice Alert ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3489
+ if ( invoiceEmails ) {
3490
+ const result = await sendEmailWithSES( invoiceEmails?.email, mailSubject, teamsMsg, '', fromEmail, [] );
3491
+ console.log( '🚀 ~ dailyPricingInsert ~ result:', result );
3480
3492
  }
3493
+
3481
3494
  return res.sendSuccess( 'Price Details Inserted Successfully' );
3482
3495
  }
3483
3496
  }
@@ -314,6 +314,7 @@ export const createBillingGroupBody = joi.object(
314
314
  proRata: joi.string().optional().allow( '' ),
315
315
  paymentCategory: joi.string().optional().allow( '' ),
316
316
  currency: joi.string().optional().allow( '' ),
317
+ taxCalculationType: joi.string().valid( 'domestic', 'international' ).optional(),
317
318
  isInstallationOneTime: joi.boolean().optional(),
318
319
  installationFee: joi.number().optional(),
319
320
  paymentCycle: joi.string().optional().allow( '' ),
@@ -352,6 +353,7 @@ export const updateBillingGroupBody = joi.object(
352
353
  proRata: joi.string().optional().allow( '' ),
353
354
  paymentCategory: joi.string().optional().allow( '' ),
354
355
  currency: joi.string().optional().allow( '' ),
356
+ taxCalculationType: joi.string().valid( 'domestic', 'international' ).optional(),
355
357
  isInstallationOneTime: joi.boolean().optional(),
356
358
  installationFee: joi.number().optional(),
357
359
  paymentCycle: joi.string().optional().allow( '' ),
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  export const billingRouter = express.Router();
3
3
  import { accessVerification, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
4
- import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup } from '../controllers/billing.controllers.js';
4
+ import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup, gstinLookup } from '../controllers/billing.controllers.js';
5
5
  import { billingGroupSchema, clientProductsValid, createBillingGroupsSchema, deleteBillingGroupsSchema, getBillingGroupsSchema, getInvoiceSchema, leadProductsValid, onetimeFeeValid, subscribedStoreListSchema, updateBillingGroupsSchema } from '../dtos/validation.dtos.js';
6
6
 
7
7
 
@@ -27,3 +27,4 @@ billingRouter.get( '/getLeadProducts/:email', isAllowedSessionHandler, validate(
27
27
  billingRouter.get( '/getClientProducts/:id', isAllowedSessionHandler, validate( clientProductsValid ), getClientProducts );
28
28
 
29
29
 
30
+ billingRouter.get( '/gst-lookup/:gstin', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), gstinLookup );
@@ -1,5 +1,5 @@
1
1
  import express from 'express';
2
- import { createInvoice, invoiceDownload, invoiceDownloadBulk, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, invoiceAnnexure, updateInvoice, getClientBasePricing, deleteInvoice, approveInvoiceCsm, approveInvoiceFinance, approveInvoiceApproval } from '../controllers/invoice.controller.js';
2
+ import { createInvoice, invoiceDownload, invoiceDownloadBulk, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, invoiceAnnexure, updateInvoice, getClientBasePricing, deleteInvoice, approveInvoiceCsm, approveInvoiceFinance, approveInvoiceApproval, recordPayment } from '../controllers/invoice.controller.js';
3
3
  import { isAllowedSessionHandler, accessVerification, validate } from 'tango-app-api-middleware';
4
4
  import { getInvoiceHeads, updateInvoiceHeads } from '../controllers/applicationDefault.controllers.js';
5
5
  import { updateInvoiceHeadsSchema } from '../dtos/validation.dtos.js';
@@ -29,6 +29,7 @@ invoiceRouter.post( '/pendingInvoices', isAllowedSessionHandler, pendingInvoices
29
29
  invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), applyDiscount );
30
30
  invoiceRouter.post( '/migrateInvoice', migrateInvoice );
31
31
  invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
32
+ invoiceRouter.post( '/recordPayment', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), recordPayment );
32
33
  invoiceRouter.post( '/checkPaymentStatus', checkPaymentStatus );
33
34
  invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
34
35
  invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
@@ -9,6 +9,11 @@ export const findOneAccount = ( query = {}, record = {} ) => {
9
9
  export const updateOneAccount = async ( query ={}, record={} ) => {
10
10
  return await model.paymentAccountModel.updateOne( query, { $set: record } );
11
11
  };
12
+
13
+ export const createAccount = async ( record ) => {
14
+ return await model.paymentAccountModel.create( record );
15
+ };
16
+
12
17
  export const aggregate = ( query = [] ) => {
13
18
  return model.paymentAccountModel.aggregate( query );
14
19
  };