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 +2 -2
- package/scripts/migrate-billing-prorata-pricing.js +66 -0
- package/src/controllers/billing.controllers.js +142 -1
- package/src/controllers/brandsBilling.controller.js +1 -0
- package/src/controllers/invoice.controller.js +229 -31
- package/src/controllers/paymentSubscription.controllers.js +20 -7
- package/src/dtos/validation.dtos.js +2 -0
- package/src/routes/billing.routes.js +2 -1
- package/src/routes/invoice.routes.js +2 -1
- package/src/services/paymentAccount.service.js +5 -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.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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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: { $
|
|
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
|
-
|
|
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:
|
|
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: { $
|
|
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 (
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
{
|
|
1793
|
-
$
|
|
1794
|
-
$
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
};
|