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

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.22",
3
+ "version": "3.5.24",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -20,16 +20,16 @@
20
20
  "dotenv": "^16.4.5",
21
21
  "exceljs": "^4.4.0",
22
22
  "express": "^4.18.3",
23
+ "express-fileupload": "^1.5.2",
23
24
  "handlebars": "^4.7.8",
24
25
  "html-pdf-node": "^1.0.8",
25
26
  "joi-to-swagger": "^6.2.0",
26
27
  "jsdom": "^24.0.0",
27
28
  "mongodb": "^6.4.0",
28
- "multer": "^1.4.5-lts.1",
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.6.37",
32
+ "tango-api-schema": "^2.6.43",
33
33
  "tango-app-api-middleware": "^3.6.18",
34
34
  "winston": "^3.12.0",
35
35
  "winston-daily-rotate-file": "^5.0.0",
@@ -343,14 +343,15 @@ export const updateBillingGroup = async ( req, res ) => {
343
343
 
344
344
  if ( removedStores.length ) {
345
345
  await updateOne( { _id: new mongoose.Types.ObjectId( req.body._id ) }, { $pull: { stores: { $in: removedStores } } } );
346
- await updateOne( { clientId: req.body.clientId, isPrimary: true }, { $push: { stores: { $each: removedStores } } } );
346
+ // $addToSet (not $push) so stores already in the primary group aren't duplicated.
347
+ await updateOne( { clientId: req.body.clientId, isPrimary: true }, { $addToSet: { stores: { $each: removedStores } } } );
347
348
  }
348
349
 
349
350
  const addedStores = req.body?.stores?.filter( ( val ) => !previousStores?.stores.includes( val ) );
350
351
 
351
352
  if ( addedStores.length ) {
352
353
  await updateMany( { clientId: req.body.clientId, _id: { $ne: new mongoose.Types.ObjectId( req.body._id ) } }, { $pull: { stores: { $in: req.body.stores } } } );
353
- await updateOne( { _id: new mongoose.Types.ObjectId( req.body._id ) }, { $push: { stores: { $each: addedStores } } } );
354
+ await updateOne( { _id: new mongoose.Types.ObjectId( req.body._id ) }, { $addToSet: { stores: { $each: addedStores } } } );
354
355
  }
355
356
  }
356
357
 
@@ -436,13 +437,32 @@ export const updateBillingGroup = async ( req, res ) => {
436
437
 
437
438
  export const deleteBillingGroup = async ( req, res ) => {
438
439
  try {
439
- const previousGroup = await findOne( { _id: new mongoose.Types.ObjectId( req.query._id ) }, { stores: 1, clientId: 1, groupName: 1 } );
440
+ const previousGroup = await findOne( { _id: new mongoose.Types.ObjectId( req.query._id ) }, { stores: 1, clientId: 1, groupName: 1, isPrimary: 1 } );
441
+ console.log( '🚀 ~ deleteBillingGroup ~ previousGroup:', previousGroup );
442
+
443
+ // No such group — surface a 404 instead of dereferencing null below (which
444
+ // would throw and 500). Covers a stale / already-deleted id.
445
+ if ( !previousGroup ) {
446
+ return res.sendError( 'Billing group not found', 404 );
447
+ }
448
+ // The primary group can't be deleted (stores fall back to it). Reject
449
+ // explicitly rather than silently no-op'ing the delete.
450
+ if ( previousGroup.isPrimary ) {
451
+ return res.sendError( 'The primary billing group cannot be deleted', 400 );
452
+ }
440
453
 
441
454
  if ( previousGroup?.stores?.length ) {
442
- await updateOne( { clientId: previousGroup.clientId, isPrimary: true }, { $push: { stores: { $each: previousGroup?.stores } } } );
455
+ // Move the deleted group's stores back to the primary group. Use $addToSet
456
+ // (not $push) so stores already present in the primary group aren't
457
+ // duplicated in its stores array.
458
+ await updateOne( { clientId: previousGroup.clientId, isPrimary: true }, { $addToSet: { stores: { $each: previousGroup?.stores } } } );
443
459
  }
444
460
 
445
461
  const deletedGroup = await deleteOne( { _id: new mongoose.Types.ObjectId( req.query._id ), isPrimary: false } );
462
+ // Guard against a silent no-op (e.g. a concurrent delete removed it first).
463
+ if ( !deletedGroup?.deletedCount ) {
464
+ return res.sendError( 'Billing group could not be deleted', 404 );
465
+ }
446
466
  const updateKeys = [];
447
467
  updateKeys.push( previousGroup.groupName );
448
468
  const logObj = {
@@ -939,13 +959,12 @@ export async function getClientProducts( req, res ) {
939
959
  }
940
960
  }
941
961
 
942
- // GSTIN address lookup via Sheet GST Check
943
- // (https://sheet.gstincheck.co.in). Returns the registered company name
944
- // + principal place of business address so the UI can auto-fill the
945
- // billing-group form.
962
+ // GSTIN address lookup via GST Verify (https://gstverify.co.in). Returns
963
+ // the registered company name + principal place of business address so the
964
+ // UI can auto-fill the billing-group form.
946
965
  //
947
- // Requires GST_LOOKUP_API_KEY in the API environment. The key is embedded
948
- // in the provider's URL path (their auth scheme); we never log it.
966
+ // Requires GST_LOOKUP_API_KEY in the API environment. The key is sent in
967
+ // the `X-API-Key` request header (the provider's auth scheme); we never log it.
949
968
  //
950
969
  // Caller validates GSTIN format (15 chars, alphanumeric). Server-side we
951
970
  // re-check before forwarding to the provider.
@@ -976,12 +995,11 @@ export async function gstinLookup( req, res ) {
976
995
  return res.sendError( 'Unknown state code in GSTIN', 404 );
977
996
  }
978
997
 
979
- // Real lookup via Sheet GST Check (https://gstincheck.co.in/).
980
- // Endpoint: GET https://sheet.gstincheck.co.in/check/{API_KEY}/{GSTIN}
981
- // Auth: API key embedded in the URL path (no header). Set
982
- // GST_LOOKUP_API_KEY in the API's environment.
998
+ // Real lookup via GST Verify (https://gstverify.co.in/).
999
+ // Endpoint: GET https://gstverify.co.in/api/v1/verify/{GSTIN}
1000
+ // Auth: API key sent in the `X-API-Key` header. Set GST_LOOKUP_API_KEY
1001
+ // in the API's environment.
983
1002
  const apiKey = process.env.GST_LOOKUP_API_KEY;
984
- console.log( '🚀 ~ gstinLookup ~ apiKey:', apiKey );
985
1003
  if ( !apiKey ) {
986
1004
  // Fail explicitly rather than silently fall back — that way a missing
987
1005
  // key shows up immediately instead of mock data leaking into prod.
@@ -992,8 +1010,8 @@ export async function gstinLookup( req, res ) {
992
1010
  let providerResponse;
993
1011
  try {
994
1012
  providerResponse = await axios.get(
995
- `https://sheet.gstincheck.co.in/check/${encodeURIComponent( apiKey )}/${encodeURIComponent( gstin )}`,
996
- { timeout: 8000 },
1013
+ `https://gstverify.co.in/api/v1/verify/${encodeURIComponent( gstin )}`,
1014
+ { timeout: 8000, headers: { 'X-API-Key': apiKey } },
997
1015
  );
998
1016
  } catch ( axiosErr ) {
999
1017
  // Distinguish provider-down vs other errors so the client can show a
@@ -1008,69 +1026,84 @@ export async function gstinLookup( req, res ) {
1008
1026
  }
1009
1027
 
1010
1028
  const body = providerResponse?.data || {};
1011
- if ( !body.flag || !body.data ) {
1029
+ if ( !body.success || !body.data ) {
1012
1030
  // Provider reached but couldn't resolve this GSTIN.
1013
1031
  return res.sendError( 'GSTIN not found', 404 );
1014
1032
  }
1015
1033
 
1016
1034
  // Map provider's response shape into the contract the frontend expects.
1017
- // Provider uses Indian-government short names (lgnm, tradeNam, pradr,
1018
- // sts, etc.); we normalize to our flat structure.
1035
+ // GST Verify returns a flat object: gstin, legal_name, trade_name,
1036
+ // status, state, pan, and a single concatenated `address` string
1037
+ // (e.g. "123 Business Park, Mumbai — 400001"). We normalize to our
1038
+ // flat structure and parse the address string into the discrete fields
1039
+ // the form needs.
1019
1040
  const src = body.data;
1020
- // TEMP: full raw response logged so we can confirm field names per
1021
- // GSTIN remove once the mapping is verified across a few real cases.
1022
- logger.error( { function: 'gstinLookup.raw', gstin, providerData: src } );
1023
-
1024
- const principal = src.pradr || src.pradr1 || {};
1025
- // The provider has used both `addr` (newer) and a flatter form
1026
- // (older). Coalesce — `addr` if present, otherwise the principal
1027
- // itself contains the address fields.
1041
+
1042
+ // Some records may still expose the structured GSTN principal-address
1043
+ // object; prefer explicit fields when present, else parse the string.
1044
+ const principal = src.pradr || {};
1028
1045
  const addr = principal.addr || principal || {};
1029
1046
 
1030
- // If the provider returned a fully-formed display address string, use
1031
- // it directly for addressLineOne — that's the canonical comma-joined
1032
- // address GSTN itself displays. Otherwise build it from the
1033
- // individual fields. This avoids re-joining in a different order than
1034
- // the registry intended.
1035
- const fullAddr = ( principal.adr || principal.address || src.adr || '' ).trim();
1047
+ // Pull the display address, whether it comes back as a plain string or
1048
+ // nested under the structured principal-address object.
1049
+ const fullAddr = String(
1050
+ src.address || principal.adr || principal.address || src.adr || '',
1051
+ ).trim();
1052
+
1053
+ // Pincode: prefer an explicit field, otherwise pull the first 6-digit
1054
+ // run out of the address string.
1055
+ const pinFromString = ( fullAddr.match( /\b(\d{6})\b/ ) || [] )[1] || '';
1056
+ const pinCode = addr.pncd ? String( addr.pncd ) :
1057
+ ( addr.pincode ? String( addr.pincode ) : pinFromString );
1058
+
1059
+ // State: provider's explicit state wins; fall back to our state-code map
1060
+ // so a missing field never blanks out the Place of Supply.
1061
+ const stateValue = src.state || addr.stcd || addr.state || stateName;
1062
+
1063
+ let addressLineOne = '';
1064
+ let addressLineTwo = '';
1065
+ let cityValue = addr.city || addr.dst || addr.loc || '';
1036
1066
 
1037
- let addressLineOne;
1038
- let addressLineTwo;
1039
1067
  if ( fullAddr ) {
1040
- // The registry format is `bno, bnm, flno, st, loc, lndmrk, city,
1041
- // dst, stcd, pncd`. Split on commas, take the first half for line 1
1042
- // and the rest (minus city/state/pincode tail which goes to the
1043
- // separate fields) for line 2. Heuristic: split into two halves.
1044
- const parts = fullAddr.split( ',' ).map( ( s ) => s.trim() ).filter( Boolean );
1068
+ // Split the display address on commas / em-dash and drop the trailing
1069
+ // tokens that belong in the discrete fields (state, pincode).
1070
+ let parts = fullAddr
1071
+ .split( /[,—-]/ )
1072
+ .map( ( s ) => s.trim() )
1073
+ .filter( Boolean )
1074
+ .filter( ( p ) => !/^\d{6}$/.test( p ) )
1075
+ .filter( ( p ) => p.toLowerCase() !== String( stateValue ).toLowerCase() );
1076
+
1077
+ // Last remaining token is typically the city/district — lift it out
1078
+ // when we don't already have an explicit city.
1079
+ if ( !cityValue && parts.length ) {
1080
+ cityValue = parts[parts.length - 1];
1081
+ parts = parts.slice( 0, -1 );
1082
+ }
1083
+
1084
+ // Split the rest across the two address lines.
1045
1085
  const half = Math.ceil( parts.length / 2 );
1046
1086
  addressLineOne = parts.slice( 0, half ).join( ', ' );
1047
1087
  addressLineTwo = parts.slice( half ).join( ', ' );
1048
- } else {
1088
+ } else if ( Object.keys( addr ).length ) {
1049
1089
  const line1Parts = [ addr.bno, addr.bnm, addr.flno, addr.st ].filter( ( p ) => p && String( p ).trim() );
1050
1090
  const line2Parts = [ addr.loc, addr.lndmrk, addr.dst ].filter( ( p ) => p && String( p ).trim() );
1051
1091
  addressLineOne = line1Parts.join( ', ' );
1052
1092
  addressLineTwo = line2Parts.join( ', ' );
1053
1093
  }
1054
1094
 
1055
- // City: prefer explicit `city` / `dst`. Some records put the city
1056
- // name in `loc` and the colony / sub-area in `dst` — prefer city, but
1057
- // fall back to dst then loc when missing.
1058
- const cityValue = addr.city || addr.dst || addr.loc || '';
1059
-
1060
1095
  const data = {
1061
1096
  gstin: src.gstin || gstin,
1062
- legalName: src.lgnm || src.legalName || '',
1063
- tradeName: src.tradeNam || src.tradeName || '',
1097
+ legalName: src.legal_name || src.lgnm || src.legalName || '',
1098
+ tradeName: src.trade_name || src.tradeNam || src.tradeName || '',
1064
1099
  addressLineOne,
1065
1100
  addressLineTwo,
1066
1101
  city: cityValue,
1067
- // Prefer provider's state name; fall back to our state-code map so a
1068
- // missing field doesn't blank out the Place of Supply.
1069
- state: addr.stcd || addr.state || stateName,
1102
+ state: stateValue,
1070
1103
  country: 'India',
1071
- pinCode: addr.pncd ? String( addr.pncd ) : ( addr.pincode ? String( addr.pincode ) : '' ),
1072
- placeOfSupply: `${addr.stcd || addr.state || stateName} (${code})`,
1073
- status: src.sts || '',
1104
+ pinCode,
1105
+ placeOfSupply: `${stateValue} (${code})`,
1106
+ status: src.status || src.sts || '',
1074
1107
  };
1075
1108
 
1076
1109
  return res.sendSuccess( data );