tango-app-api-payment-subscription 3.5.3 → 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.3",
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,
@@ -113,6 +113,72 @@ export async function createInvoice( req, res ) {
113
113
  invoiceGroupList.push( invoiceGroup );
114
114
  }
115
115
 
116
+ // Custom-create path: the Create Invoice modal posts a fully-composed
117
+ // invoice (companyName, products, tax, totals — all entered by the user).
118
+ // The legacy aggregation paths above don't handle this shape and would
119
+ // silently no-op (empty invoiceGroupList → loop skipped → 2xx with nothing
120
+ // written). When the caller sets customInvoice=true we insert exactly
121
+ // what they sent, with a server-generated invoice number.
122
+ if ( req.body.customInvoice === true ) {
123
+ if ( !req.body.clientId ) {
124
+ return res.sendError( 'clientId is required for customInvoice', 400 );
125
+ }
126
+ const Finacialyear = getCurrentFinancialYear();
127
+ const previousinvoice = await invoiceService.findandsort(
128
+ { invoice: { $regex: `^INV-${Finacialyear}-` } },
129
+ {},
130
+ { invoiceIndex: -1 },
131
+ );
132
+ let invoiceNo = '00001';
133
+ if ( previousinvoice && previousinvoice.length > 0 ) {
134
+ invoiceNo = Number( previousinvoice[0].invoiceIndex ) + 1;
135
+ invoiceNo = invoiceNo.toString().padStart( 5, '0' );
136
+ }
137
+ const baseDate = req.body.billingDate ? dayjs( req.body.billingDate ) : dayjs();
138
+ const data = {
139
+ invoice: `INV-${Finacialyear}-${invoiceNo}`,
140
+ invoiceIndex: invoiceNo,
141
+ clientId: req.body.clientId,
142
+ groupId: req.body.groupId || undefined,
143
+ groupName: req.body.groupName || '',
144
+ companyName: req.body.companyName || '',
145
+ companyAddress: req.body.companyAddress || '',
146
+ GSTNumber: req.body.GSTNumber || '',
147
+ PlaceOfSupply: req.body.PlaceOfSupply || '',
148
+ products: Array.isArray( req.body.products ) ? req.body.products : [],
149
+ tax: Array.isArray( req.body.tax ) ? req.body.tax : [],
150
+ amount: Math.round( Number( req.body.amount ) || 0 ),
151
+ totalAmount: Math.round( Number( req.body.totalAmount ) || 0 ),
152
+ stores: Number( req.body.stores ) || 0,
153
+ currency: req.body.currency || 'inr',
154
+ billingDate: baseDate.toDate(),
155
+ dueDate: req.body.dueDate ? new Date( req.body.dueDate ) : baseDate.add( 30, 'days' ).toDate(),
156
+ monthOfbilling: baseDate.format( 'MM' ),
157
+ paymentMethod: 'Online',
158
+ status: 'pendingCsm',
159
+ paymentStatus: 'unpaid',
160
+ };
161
+ const created = await invoiceService.create( data );
162
+ const logObj = {
163
+ userName: req.user?.userName,
164
+ email: req.user?.email,
165
+ clientId: data.clientId,
166
+ logSubType: 'invoiceCreated',
167
+ logType: 'invoice',
168
+ date: new Date(),
169
+ changes: [ `${data.invoice} invoice created manually by ${req.user?.email}` ],
170
+ eventType: 'create',
171
+ timestamp: new Date(),
172
+ showTo: [ 'tango' ],
173
+ };
174
+ try {
175
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
176
+ } catch ( logErr ) {
177
+ logger.error( { error: logErr, function: 'createInvoice.customLog' } );
178
+ }
179
+ return res.sendSuccess( { data: created } );
180
+ }
181
+
116
182
  for ( let group of invoiceGroupList ) {
117
183
  let Finacialyear = getCurrentFinancialYear();
118
184
  // Scope the highest-index lookup to invoices created in the current FY
@@ -142,7 +208,14 @@ export async function createInvoice( req, res ) {
142
208
  let amount = products.reduce( ( sum, product ) => sum + product.amount, 0 );
143
209
  let taxList = [];
144
210
  let totalAmount = 0;
145
- 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' ) {
146
219
  let taxAmount = ( amount * 18 ) / 100;
147
220
  totalAmount = Math.round( amount + taxAmount );
148
221
  taxList.push(
@@ -303,7 +376,7 @@ export async function invoiceDownload( req, res ) {
303
376
  let [ firstWord, secondWord ] = item.productName.replace( /([a-z])([A-Z])/g, '$1 $2' ).split( ' ' );
304
377
  firstWord = firstWord.charAt( 0 ).toUpperCase() + firstWord.slice( 1 );
305
378
  item.productName = firstWord + ' ' + secondWord;
306
- item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
379
+ item.price = item.price .toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
307
380
  item.amount = item.amount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
308
381
  item.currency = invoiceCurrency;
309
382
  } );
@@ -939,6 +1012,11 @@ function inWords( num ) {
939
1012
  async function standardPrice( group, getClient, baseDate ) {
940
1013
  console.log( '🚀 ~ standardPrice ~ baseDate:', baseDate.format( 'MMM YYYY' ) );
941
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 );
942
1020
  let billingTypeMap = {};
943
1021
  if ( getClient?.planDetails?.product ) {
944
1022
  getClient.planDetails.product.forEach( ( p ) => {
@@ -986,6 +1064,13 @@ async function standardPrice( group, getClient, baseDate ) {
986
1064
  storeStatus: '$stores.status',
987
1065
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
988
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 ] },
989
1074
  },
990
1075
  },
991
1076
  {
@@ -997,23 +1082,31 @@ async function standardPrice( group, getClient, baseDate ) {
997
1082
  $project: {
998
1083
  productName: 1,
999
1084
  storeId: 1,
1000
- 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 },
1001
1088
  workingDays: 1,
1002
1089
  storeStatus: 1,
1003
1090
  zoneCount: 1,
1004
1091
  cameraCount: 1,
1092
+ trafficCameraCount: 1,
1093
+ zoneCameraCount: 1,
1005
1094
  },
1006
1095
  },
1007
1096
  {
1008
1097
  $project: {
1009
1098
  productName: 1,
1010
1099
  storeId: 1,
1100
+ // Flat pricing => every store billed for the full month.
1101
+ // Prorate => keep the actual workingDays.
1011
1102
  workingDays: {
1012
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1103
+ $cond: { if: '$isFlatPricing', then: currentMonthDays, else: '$workingDays' },
1013
1104
  },
1014
1105
  storeStatus: 1,
1015
1106
  zoneCount: 1,
1016
1107
  cameraCount: 1,
1108
+ trafficCameraCount: 1,
1109
+ zoneCameraCount: 1,
1017
1110
  },
1018
1111
  },
1019
1112
  {
@@ -1025,6 +1118,8 @@ async function standardPrice( group, getClient, baseDate ) {
1025
1118
  workingdays: { $first: '$workingDays' },
1026
1119
  zoneCount: { $first: '$zoneCount' },
1027
1120
  cameraCount: { $first: '$cameraCount' },
1121
+ trafficCameraCount: { $first: '$trafficCameraCount' },
1122
+ zoneCameraCount: { $first: '$zoneCameraCount' },
1028
1123
  },
1029
1124
  },
1030
1125
  {
@@ -1228,16 +1323,24 @@ async function standardPrice( group, getClient, baseDate ) {
1228
1323
  storeStatus: '$stores.status',
1229
1324
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1230
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 ] },
1231
1331
  },
1232
1332
  },
1233
1333
  { $match: { workingDays: { $gt: 0 }, productName: { $in: eachStoreProductNames } } },
1234
1334
  {
1235
1335
  $project: {
1236
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.
1237
1339
  workingDays: {
1238
- $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' },
1239
1341
  },
1240
1342
  zoneCount: 1, cameraCount: 1,
1343
+ trafficCameraCount: 1, zoneCameraCount: 1,
1241
1344
  },
1242
1345
  },
1243
1346
  {
@@ -1265,6 +1368,11 @@ async function standardPrice( group, getClient, baseDate ) {
1265
1368
  {
1266
1369
  $project: {
1267
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,
1268
1376
  price: '$matchedStandard.negotiatePrice',
1269
1377
  period: { $cond: { if: { $lt: [ '$workingDays', currentMonthDays ] }, then: 'prorate', else: 'fullmonth' } },
1270
1378
  },
@@ -1273,6 +1381,7 @@ async function standardPrice( group, getClient, baseDate ) {
1273
1381
 
1274
1382
  for ( let store of perStoreData ) {
1275
1383
  let productBillingType = billingTypeMap[store.productName] || 'perStore';
1384
+
1276
1385
  let storeCount = 1;
1277
1386
  if ( store.productName === 'tangoZone' ) {
1278
1387
  if ( productBillingType === 'perZone' && store.zoneCount > 0 ) {
@@ -1340,6 +1449,9 @@ async function standardPrice( group, getClient, baseDate ) {
1340
1449
 
1341
1450
  async function stepPrice( group, getClient ) {
1342
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';
1343
1455
  let billingTypeMap = {};
1344
1456
  if ( getClient?.planDetails?.product ) {
1345
1457
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1387,6 +1499,11 @@ async function stepPrice( group, getClient ) {
1387
1499
  storeStatus: '$stores.status',
1388
1500
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1389
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 ] },
1390
1507
  },
1391
1508
  },
1392
1509
  {
@@ -1398,23 +1515,30 @@ async function stepPrice( group, getClient ) {
1398
1515
  $project: {
1399
1516
  productName: 1,
1400
1517
  storeId: 1,
1401
- prorate: { $literal: group.proRata },
1518
+ // Group-level flag baked into every doc; consumed by the next
1519
+ // $project's $cond.
1520
+ isFlatPricing: { $literal: isFlatPricing },
1402
1521
  workingDays: 1,
1403
1522
  storeStatus: 1,
1404
1523
  zoneCount: 1,
1405
1524
  cameraCount: 1,
1525
+ trafficCameraCount: 1,
1526
+ zoneCameraCount: 1,
1406
1527
  },
1407
1528
  },
1408
1529
  {
1409
1530
  $project: {
1410
1531
  productName: 1,
1411
1532
  storeId: 1,
1533
+ // Flat => full month per store. Prorate => actual working days.
1412
1534
  workingDays: {
1413
- $cond: { if: { $and: [ { $gte: [ '$workingDays', 15 ] }, { $eq: [ '$storeStatus', 'active' ] }, { $eq: [ '$prorate', 'before15' ] } ] }, then: currentMonthDays, else: '$workingDays' },
1535
+ $cond: { if: '$isFlatPricing', then: currentMonthDays, else: '$workingDays' },
1414
1536
  },
1415
1537
  storeStatus: 1,
1416
1538
  zoneCount: 1,
1417
1539
  cameraCount: 1,
1540
+ trafficCameraCount: 1,
1541
+ zoneCameraCount: 1,
1418
1542
  },
1419
1543
  }, {
1420
1544
  $group: {
@@ -1425,6 +1549,8 @@ async function stepPrice( group, getClient ) {
1425
1549
  workingdays: { $first: '$workingDays' },
1426
1550
  zoneCount: { $first: '$zoneCount' },
1427
1551
  cameraCount: { $first: '$cameraCount' },
1552
+ trafficCameraCount: { $first: '$trafficCameraCount' },
1553
+ zoneCameraCount: { $first: '$zoneCameraCount' },
1428
1554
  },
1429
1555
  },
1430
1556
  {
@@ -1493,16 +1619,24 @@ async function stepPrice( group, getClient ) {
1493
1619
  storeStatus: '$stores.status',
1494
1620
  zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1495
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 ] },
1496
1627
  },
1497
1628
  },
1498
1629
  { $match: { workingDays: { $gt: 0 }, productName: { $in: eachStoreProductNames } } },
1499
1630
  {
1500
1631
  $project: {
1501
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.
1502
1635
  workingDays: {
1503
- $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' },
1504
1637
  },
1505
1638
  zoneCount: 1, cameraCount: 1,
1639
+ trafficCameraCount: 1, zoneCameraCount: 1,
1506
1640
  },
1507
1641
  },
1508
1642
  {
@@ -1704,35 +1838,64 @@ export async function clientInvoiceList( req, res ) {
1704
1838
  clientId: { $in: findClients },
1705
1839
  },
1706
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
+
1707
1848
  let filterStartDate = '';
1708
1849
  let filterEndDate = '';
1709
1850
 
1710
- if ( req.body?.filter && req.body.filter == 'current' ) {
1711
- filterStartDate = new Date( dayjs().startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1712
- filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1713
- }
1714
- if ( req.body?.filter && req.body.filter == 'prev' ) {
1715
- filterStartDate = new Date( dayjs().subtract( 1, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1716
- filterEndDate = new Date( dayjs().subtract( 1, 'month' ).endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1717
- }
1718
- if ( req.body?.filter && req.body.filter == 'last' ) {
1719
- filterStartDate = new Date( dayjs().subtract( 12, 'month' ).startOf( 'month' ).format( 'YYYY-MM-DD' ) );
1720
- filterEndDate = new Date( dayjs().endOf( 'month' ).format( 'YYYY-MM-DD' ) );
1721
- }
1722
- 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
+ }
1723
1864
 
1724
- if ( req.body?.filter ) {
1725
- query.push(
1726
- {
1727
- $match: {
1728
- $and: [
1729
- { billingDate: { $gte: filterStartDate } },
1730
- { billingDate: { $lte: filterEndDate } },
1731
- ],
1732
- },
1865
+ if ( req.body?.filter ) {
1866
+ query.push( {
1867
+ $match: {
1868
+ $and: [
1869
+ { billingDate: { $gte: filterStartDate } },
1870
+ { billingDate: { $lte: filterEndDate } },
1871
+ ],
1733
1872
  },
1873
+ } );
1874
+ }
1875
+ }
1734
1876
 
1735
- );
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 } } );
1736
1899
  }
1737
1900
 
1738
1901
 
@@ -1769,6 +1932,7 @@ export async function clientInvoiceList( req, res ) {
1769
1932
  clientName: '$clientDetails.clientName',
1770
1933
  logo: '$clientDetails.logo',
1771
1934
  currencyType: '$currency',
1935
+ currency: 1,
1772
1936
  invoice: 1,
1773
1937
  stores: 1,
1774
1938
  amount: 1,
@@ -1776,6 +1940,7 @@ export async function clientInvoiceList( req, res ) {
1776
1940
  groupName: 1,
1777
1941
  status: 1,
1778
1942
  paymentStatus: 1,
1943
+ paidAmount: { $ifNull: [ '$paidAmount', 0 ] },
1779
1944
  clientId: 1,
1780
1945
  billingDate: 1,
1781
1946
  },
@@ -2081,6 +2246,105 @@ export async function PaymentStatusChange( req, res ) {
2081
2246
  }
2082
2247
  }
2083
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
+
2084
2348
  export async function checkPaymentStatus( req, res ) {
2085
2349
  try {
2086
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
  };