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 +3 -3
- package/src/controllers/billing.controllers.js +88 -55
- package/src/controllers/brandsBilling.controller.js +430 -69
- package/src/controllers/estimate.controller.js +111 -17
- package/src/controllers/invoice.controller.js +53 -8
- package/src/controllers/paymentSubscription.controllers.js +82 -2
- package/src/controllers/purchaseOrder.controller.js +112 -4
- package/src/hbs/estimatePdf.hbs +12 -0
- package/src/hbs/invoicepaymentemail.hbs +4 -0
- package/src/routes/billing.routes.js +7 -8
- package/src/routes/brandsBilling.routes.js +12 -12
- package/src/routes/invoice.routes.js +4 -3
- package/src/routes/paymentSubscription.routes.js +5 -7
- package/src/services/clientPayment.services.js +21 -0
- package/src/services/purchaseOrder.service.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-payment-subscription",
|
|
3
|
-
"version": "3.5.
|
|
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.
|
|
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
|
-
|
|
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 ) }, { $
|
|
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
|
-
|
|
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
|
|
943
|
-
//
|
|
944
|
-
//
|
|
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
|
|
948
|
-
//
|
|
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
|
|
980
|
-
// Endpoint: GET https://
|
|
981
|
-
// Auth: API key
|
|
982
|
-
//
|
|
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://
|
|
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.
|
|
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
|
-
//
|
|
1018
|
-
//
|
|
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
|
-
|
|
1021
|
-
//
|
|
1022
|
-
|
|
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
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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
|
|
1072
|
-
placeOfSupply: `${
|
|
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 );
|