tango-app-api-payment-subscription 3.5.6 → 3.5.7
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/src/controllers/brandsBilling.controller.js +157 -10
- package/src/controllers/invoice.controller.js +32 -24
- package/src/controllers/paymentReminder.controller.js +81 -0
- package/src/routes/billing.routes.js +5 -0
- package/src/routes/brandsBilling.routes.js +1 -0
- package/src/services/paymentReminder.service.js +9 -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.7",
|
|
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.6.
|
|
32
|
+
"tango-api-schema": "^2.6.26",
|
|
33
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
34
34
|
"winston": "^3.12.0",
|
|
35
35
|
"winston-daily-rotate-file": "^5.0.0",
|
|
@@ -164,19 +164,93 @@ export async function brandsBillingList( req, res ) {
|
|
|
164
164
|
|
|
165
165
|
let allData = await clientService.aggregate( query );
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
// Lifecycle + payment counts over the FULL client population (no status /
|
|
168
|
+
// paymentStatus filter), so the overview cards stay stable regardless of
|
|
169
|
+
// which lifecycle tab is selected — and so Hold / Suspended / Deactive
|
|
170
|
+
// show even when the default Active tab has zero rows. trialPaid is the
|
|
171
|
+
// derived "paid plan with at least one product still on trial" bucket.
|
|
172
|
+
// Bucket every client into ONE mutually-exclusive payment bucket so the
|
|
173
|
+
// pills (Paid / Trial / Paid-Trial / Free) sum to their lifecycle tab
|
|
174
|
+
// total. trialPaid = paid plan that still has a product on trial; such a
|
|
175
|
+
// client is trialPaid, NOT also paid.
|
|
176
|
+
const payBucketExpr = {
|
|
177
|
+
$let: {
|
|
178
|
+
vars: {
|
|
179
|
+
ps: '$planDetails.paymentStatus',
|
|
180
|
+
hasTrialProduct: { $gt: [ { $size: { $filter: {
|
|
181
|
+
input: { $ifNull: [ '$planDetails.product', [] ] },
|
|
182
|
+
as: 'p',
|
|
183
|
+
cond: { $eq: [ '$$p.status', 'trial' ] },
|
|
184
|
+
} } }, 0 ] },
|
|
185
|
+
},
|
|
186
|
+
in: {
|
|
187
|
+
$switch: {
|
|
188
|
+
branches: [
|
|
189
|
+
{ case: { $eq: [ '$$ps', 'trial' ] }, then: 'trial' },
|
|
190
|
+
{ case: { $eq: [ '$$ps', 'free' ] }, then: 'free' },
|
|
191
|
+
{ case: { $and: [ { $eq: [ '$$ps', 'paid' ] }, '$$hasTrialProduct' ] }, then: 'trialPaid' },
|
|
192
|
+
{ case: { $eq: [ '$$ps', 'paid' ] }, then: 'paid' },
|
|
193
|
+
],
|
|
194
|
+
default: 'other',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const matrixAgg = await clientService.aggregate( [
|
|
200
|
+
{ $project: { status: 1, payBucket: payBucketExpr } },
|
|
201
|
+
{ $group: { _id: { status: '$status', pay: '$payBucket' }, count: { $sum: 1 } } },
|
|
202
|
+
] );
|
|
203
|
+
|
|
204
|
+
// Lifecycle totals + a status×payment matrix. Counts are over the FULL
|
|
205
|
+
// population (no filter) so the overview cards and pills stay stable
|
|
206
|
+
// regardless of the selected tab, and show even when the Active tab is
|
|
207
|
+
// empty.
|
|
208
|
+
const lifecycle = { active: 0, hold: 0, suspended: 0, deactive: 0 };
|
|
209
|
+
const payTotals = { trial: 0, paid: 0, free: 0, trialPaid: 0 };
|
|
210
|
+
const paymentByStatus = {};
|
|
211
|
+
let totalBrands = 0;
|
|
212
|
+
matrixAgg.forEach( ( row ) => {
|
|
213
|
+
const st = row._id.status || 'active';
|
|
214
|
+
const pay = row._id.pay;
|
|
215
|
+
const n = row.count || 0;
|
|
216
|
+
totalBrands += n;
|
|
217
|
+
if ( lifecycle[st] != null ) {
|
|
218
|
+
lifecycle[st] += n;
|
|
219
|
+
}
|
|
220
|
+
if ( payTotals[pay] != null ) {
|
|
221
|
+
payTotals[pay] += n;
|
|
222
|
+
}
|
|
223
|
+
if ( !paymentByStatus[st] ) {
|
|
224
|
+
paymentByStatus[st] = { trial: 0, paid: 0, free: 0, trialPaid: 0 };
|
|
225
|
+
}
|
|
226
|
+
if ( paymentByStatus[st][pay] != null ) {
|
|
227
|
+
paymentByStatus[st][pay] += n;
|
|
228
|
+
}
|
|
229
|
+
} );
|
|
170
230
|
|
|
171
231
|
let summary = {
|
|
172
|
-
totalBrands
|
|
173
|
-
active:
|
|
174
|
-
|
|
175
|
-
|
|
232
|
+
totalBrands,
|
|
233
|
+
active: lifecycle.active,
|
|
234
|
+
hold: lifecycle.hold,
|
|
235
|
+
suspended: lifecycle.suspended,
|
|
236
|
+
deactive: lifecycle.deactive,
|
|
237
|
+
trial: payTotals.trial,
|
|
238
|
+
paid: payTotals.paid,
|
|
239
|
+
free: payTotals.free,
|
|
240
|
+
trialPaid: payTotals.trialPaid,
|
|
241
|
+
paymentByStatus,
|
|
242
|
+
// Money/store totals stay tied to the filtered view so they match the
|
|
243
|
+
// rows on screen.
|
|
176
244
|
totalBillDue: allData.reduce( ( sum, c ) => sum + ( c.billAmountDue || 0 ), 0 ),
|
|
177
245
|
storesUnderBilling: allData.reduce( ( sum, c ) => sum + ( c.billingStores || 0 ), 0 ),
|
|
178
246
|
};
|
|
179
247
|
|
|
248
|
+
if ( allData.length == 0 ) {
|
|
249
|
+
// Still return the population summary so the overview cards populate even
|
|
250
|
+
// when the current lifecycle tab is empty.
|
|
251
|
+
return res.sendSuccess( { summary, count: 0, data: [] } );
|
|
252
|
+
}
|
|
253
|
+
|
|
180
254
|
if ( req.body.export ) {
|
|
181
255
|
const exportdata = [];
|
|
182
256
|
allData.forEach( ( element ) => {
|
|
@@ -526,6 +600,36 @@ export async function latestDailyPricing( req, res ) {
|
|
|
526
600
|
storeList = stores.slice( skip, skip + Number( req.body.limit ) );
|
|
527
601
|
}
|
|
528
602
|
|
|
603
|
+
// Monthly Billing Summary — one row per month of the brand's invoice
|
|
604
|
+
// history (stores billed + invoice amount), newest first. The UI tags the
|
|
605
|
+
// current/last-generated rows and computes month-over-month deltas.
|
|
606
|
+
// billingDate is a Date on most rows but a string on some legacy ones, so
|
|
607
|
+
// coerce before extracting year/month.
|
|
608
|
+
const monthlyBillingSummary = await invoiceService.aggregate( [
|
|
609
|
+
{ $match: { clientId: req.body.clientId } },
|
|
610
|
+
{ $addFields: { billingDateD: { $cond: [
|
|
611
|
+
{ $eq: [ { $type: '$billingDate' }, 'date' ] },
|
|
612
|
+
'$billingDate',
|
|
613
|
+
{ $toDate: '$billingDate' },
|
|
614
|
+
] } } },
|
|
615
|
+
{ $match: { billingDateD: { $ne: null } } },
|
|
616
|
+
{ $group: {
|
|
617
|
+
_id: { year: { $year: '$billingDateD' }, month: { $month: '$billingDateD' } },
|
|
618
|
+
storesBilled: { $sum: { $ifNull: [ '$stores', 0 ] } },
|
|
619
|
+
invoiceAmount: { $sum: { $ifNull: [ '$totalAmount', 0 ] } },
|
|
620
|
+
currency: { $last: { $ifNull: [ '$currency', 'inr' ] } },
|
|
621
|
+
} },
|
|
622
|
+
{ $sort: { '_id.year': -1, '_id.month': -1 } },
|
|
623
|
+
{ $project: {
|
|
624
|
+
_id: 0,
|
|
625
|
+
year: '$_id.year',
|
|
626
|
+
month: '$_id.month',
|
|
627
|
+
storesBilled: 1,
|
|
628
|
+
invoiceAmount: { $round: [ '$invoiceAmount', 2 ] },
|
|
629
|
+
currency: 1,
|
|
630
|
+
} },
|
|
631
|
+
] );
|
|
632
|
+
|
|
529
633
|
let data = {
|
|
530
634
|
clientId: record.clientId,
|
|
531
635
|
brandName: record.brandName,
|
|
@@ -537,6 +641,7 @@ export async function latestDailyPricing( req, res ) {
|
|
|
537
641
|
proRate: record.proRate,
|
|
538
642
|
count,
|
|
539
643
|
data: storeList,
|
|
644
|
+
monthlyBillingSummary,
|
|
540
645
|
};
|
|
541
646
|
|
|
542
647
|
res.sendSuccess( data );
|
|
@@ -1305,7 +1410,7 @@ export async function bulkUpdateBillingGroups( req, res ) {
|
|
|
1305
1410
|
// with an env override (USD_INR_RATE) and a last-known/static fallback so
|
|
1306
1411
|
// the summary never fails because a rate API is down.
|
|
1307
1412
|
let usdRateCache = { rate: null, at: 0 };
|
|
1308
|
-
async function getUsdInrRate() {
|
|
1413
|
+
export async function getUsdInrRate() {
|
|
1309
1414
|
const override = Number( process.env.USD_INR_RATE );
|
|
1310
1415
|
if ( override > 0 ) {
|
|
1311
1416
|
return override;
|
|
@@ -1491,7 +1596,12 @@ export async function billingSummary( req, res ) {
|
|
|
1491
1596
|
}
|
|
1492
1597
|
let pricePerStore = null;
|
|
1493
1598
|
const pdoc = pricingByClient.get( r.clientId );
|
|
1494
|
-
|
|
1599
|
+
// Price/Store counts only subscribed products — and never the one-time
|
|
1600
|
+
// installationFee line, which isn't a per-store recurring price.
|
|
1601
|
+
const isLive = ( name ) => {
|
|
1602
|
+
const n = String( name || '' ).toLowerCase();
|
|
1603
|
+
return n !== 'installationfee' && r.liveProductSet.has( n );
|
|
1604
|
+
};
|
|
1495
1605
|
if ( pdoc?.standard?.length ) {
|
|
1496
1606
|
const liveRows = pdoc.standard.filter( ( p ) => isLive( p.productName ) );
|
|
1497
1607
|
pricePerStore = liveRows.length ?
|
|
@@ -1573,7 +1683,44 @@ export async function billingSummary( req, res ) {
|
|
|
1573
1683
|
};
|
|
1574
1684
|
} ).sort( ( a, b ) => ( b.revCur || 0 ) - ( a.revCur || 0 ) );
|
|
1575
1685
|
|
|
1576
|
-
|
|
1686
|
+
// Server-side filters (GET query or POST body). CSM / Product / Variance /
|
|
1687
|
+
// search narrow the per-client rows after they're computed, since those
|
|
1688
|
+
// fields are derived during the merge above.
|
|
1689
|
+
const f = { ...( req.query || {} ), ...( req.body || {} ) };
|
|
1690
|
+
const csm = f.csm && f.csm !== 'All' ? String( f.csm ) : '';
|
|
1691
|
+
const product = f.product && f.product !== 'All' ? String( f.product ) : '';
|
|
1692
|
+
const variance = f.variance && f.variance !== 'All' ? String( f.variance ) : '';
|
|
1693
|
+
const search = f.search ? String( f.search ).toLowerCase().trim() : '';
|
|
1694
|
+
|
|
1695
|
+
let filtered = data;
|
|
1696
|
+
if ( csm ) {
|
|
1697
|
+
filtered = filtered.filter( ( r ) => ( r.csm || '' ).split( ', ' ).includes( csm ) );
|
|
1698
|
+
}
|
|
1699
|
+
if ( product ) {
|
|
1700
|
+
filtered = filtered.filter( ( r ) => ( r.products || [] ).includes( product ) );
|
|
1701
|
+
}
|
|
1702
|
+
if ( variance === 'growth' ) {
|
|
1703
|
+
filtered = filtered.filter( ( r ) => r.variance > 0 );
|
|
1704
|
+
} else if ( variance === 'decline' ) {
|
|
1705
|
+
filtered = filtered.filter( ( r ) => r.variance < 0 );
|
|
1706
|
+
} else if ( variance === 'flat' ) {
|
|
1707
|
+
filtered = filtered.filter( ( r ) => r.variance === 0 );
|
|
1708
|
+
}
|
|
1709
|
+
if ( search ) {
|
|
1710
|
+
filtered = filtered.filter( ( r ) =>
|
|
1711
|
+
( r.clientName || '' ).toLowerCase().includes( search ) ||
|
|
1712
|
+
( r.registeredEntity || '' ).toLowerCase().includes( search ) ||
|
|
1713
|
+
String( r.clientId || '' ).includes( search ),
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Distinct option lists for the filter popover are derived from the FULL
|
|
1718
|
+
// result set (not the filtered slice) so the dropdowns don't shrink as
|
|
1719
|
+
// filters are applied.
|
|
1720
|
+
const csmOptions = [ ...new Set( data.flatMap( ( r ) => String( r.csm || '' ).split( ', ' ).filter( Boolean ) ) ) ].sort();
|
|
1721
|
+
const productOptions = [ ...new Set( data.flatMap( ( r ) => r.products || [] ) ) ].sort();
|
|
1722
|
+
|
|
1723
|
+
return res.sendSuccess( { months, data: filtered, total: data.length, csmOptions, productOptions, usdRate } );
|
|
1577
1724
|
} catch ( error ) {
|
|
1578
1725
|
logger.error( { error: error, function: 'billingSummary' } );
|
|
1579
1726
|
return res.sendError( error, 500 );
|
|
@@ -17,6 +17,7 @@ import { symbolFor } from '../utils/currency.js';
|
|
|
17
17
|
import { invoiceStatusEnum } from '../dtos/validation.dtos.js';
|
|
18
18
|
import { findOneApplicationDefault } from '../services/applicationDefault.service.js';
|
|
19
19
|
import * as assignedStoreService from '../services/assignedStore.service.js';
|
|
20
|
+
import { getUsdInrRate } from './brandsBilling.controller.js';
|
|
20
21
|
|
|
21
22
|
// Pulls CSM + Finance head emails (stored under applicationDefault
|
|
22
23
|
// type=invoice, subType=heads) AND the per-client CSMs assigned via
|
|
@@ -1895,35 +1896,42 @@ export async function clientInvoiceList( req, res ) {
|
|
|
1895
1896
|
}
|
|
1896
1897
|
}
|
|
1897
1898
|
|
|
1898
|
-
// Card totals — computed over the
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
//
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1899
|
+
// Card totals — computed over the CURRENTLY-FILTERED result set (`count`
|
|
1900
|
+
// holds every invoice matching the active filters, pre-pagination), so the
|
|
1901
|
+
// cards reflect exactly what the filters select. Dollar invoices are
|
|
1902
|
+
// converted to INR at today's rate so all three totals are a single ₹
|
|
1903
|
+
// figure. Outstanding = unpaid remaining (totalAmount - paidAmount);
|
|
1904
|
+
// Overdue = past-due unpaid subset; Pending Payment = approved-but-unpaid.
|
|
1905
|
+
const usdRate = await getUsdInrRate();
|
|
1904
1906
|
const now = new Date();
|
|
1905
|
-
const
|
|
1906
|
-
{ $ifNull: [ '$totalAmount', { $ifNull: [ '$amount', 0 ] } ] },
|
|
1907
|
-
{ $ifNull: [ '$paidAmount', 0 ] },
|
|
1908
|
-
] };
|
|
1909
|
-
const cardsAggregate = await invoiceService.aggregate( [
|
|
1910
|
-
{ $match: { clientId: { $in: findClients }, paymentStatus: { $ne: 'paid' } } },
|
|
1911
|
-
{ $group: {
|
|
1912
|
-
_id: null,
|
|
1913
|
-
outstandingAmount: { $sum: remaining },
|
|
1914
|
-
outstandingCount: { $sum: 1 },
|
|
1915
|
-
overdueAmount: { $sum: { $cond: [ { $lt: [ '$dueDate', now ] }, remaining, 0 ] } },
|
|
1916
|
-
overdueCount: { $sum: { $cond: [ { $lt: [ '$dueDate', now ] }, 1, 0 ] } },
|
|
1917
|
-
pendingPaymentAmount: { $sum: { $cond: [ { $eq: [ '$status', 'approved' ] }, remaining, 0 ] } },
|
|
1918
|
-
pendingPaymentCount: { $sum: { $cond: [ { $eq: [ '$status', 'approved' ] }, 1, 0 ] } },
|
|
1919
|
-
} },
|
|
1920
|
-
] );
|
|
1921
|
-
const cards = cardsAggregate[0] || {
|
|
1907
|
+
const cards = {
|
|
1922
1908
|
outstandingAmount: 0, outstandingCount: 0,
|
|
1923
1909
|
overdueAmount: 0, overdueCount: 0,
|
|
1924
1910
|
pendingPaymentAmount: 0, pendingPaymentCount: 0,
|
|
1925
1911
|
};
|
|
1926
|
-
|
|
1912
|
+
for ( const inv of count ) {
|
|
1913
|
+
if ( inv.paymentStatus === 'paid' ) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
const fx = inv.currency === 'dollar' ? usdRate : 1;
|
|
1917
|
+
const total = Number( inv.totalAmount ) || Number( inv.amount ) || 0;
|
|
1918
|
+
const paid = Number( inv.paidAmount ) || 0;
|
|
1919
|
+
const remaining = Math.max( 0, total - paid ) * fx;
|
|
1920
|
+
|
|
1921
|
+
cards.outstandingAmount += remaining;
|
|
1922
|
+
cards.outstandingCount += 1;
|
|
1923
|
+
if ( inv.dueDate && new Date( inv.dueDate ) < now ) {
|
|
1924
|
+
cards.overdueAmount += remaining;
|
|
1925
|
+
cards.overdueCount += 1;
|
|
1926
|
+
}
|
|
1927
|
+
if ( inv.status === 'approved' ) {
|
|
1928
|
+
cards.pendingPaymentAmount += remaining;
|
|
1929
|
+
cards.pendingPaymentCount += 1;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
cards.outstandingAmount = Math.round( cards.outstandingAmount * 100 ) / 100;
|
|
1933
|
+
cards.overdueAmount = Math.round( cards.overdueAmount * 100 ) / 100;
|
|
1934
|
+
cards.pendingPaymentAmount = Math.round( cards.pendingPaymentAmount * 100 ) / 100;
|
|
1927
1935
|
|
|
1928
1936
|
res.sendSuccess( { count: count.length, data: invoiceList, cards } );
|
|
1929
1937
|
} catch ( error ) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as paymentReminderService from '../services/paymentReminder.service.js';
|
|
2
|
+
import { logger } from 'tango-app-api-middleware';
|
|
3
|
+
|
|
4
|
+
// Payment reminder config (Billing Settings page). One document per brand
|
|
5
|
+
// (clientId): recipient emails + five toggleable reminder templates. Returns
|
|
6
|
+
// sensible defaults when a brand has no config saved yet.
|
|
7
|
+
const DEFAULTS = () => ( {
|
|
8
|
+
reminderEmails: [],
|
|
9
|
+
templates: {
|
|
10
|
+
preDue: { enabled: true, daysBefore: 3 },
|
|
11
|
+
onDue: { enabled: true },
|
|
12
|
+
onHold: { enabled: true },
|
|
13
|
+
suspend: { enabled: true },
|
|
14
|
+
deactivated: { enabled: false },
|
|
15
|
+
},
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
export async function getPaymentReminder( req, res ) {
|
|
19
|
+
try {
|
|
20
|
+
const clientId = req.params.clientId || req.query.clientId;
|
|
21
|
+
if ( !clientId ) {
|
|
22
|
+
return res.sendError( 'clientId is required', 400 );
|
|
23
|
+
}
|
|
24
|
+
const existing = await paymentReminderService.findOne( { clientId } );
|
|
25
|
+
if ( !existing ) {
|
|
26
|
+
return res.sendSuccess( { clientId, ...DEFAULTS(), isDefault: true } );
|
|
27
|
+
}
|
|
28
|
+
return res.sendSuccess( existing );
|
|
29
|
+
} catch ( error ) {
|
|
30
|
+
logger.error( { error: error, function: 'getPaymentReminder' } );
|
|
31
|
+
return res.sendError( error, 500 );
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function savePaymentReminder( req, res ) {
|
|
36
|
+
try {
|
|
37
|
+
const b = req.body || {};
|
|
38
|
+
if ( !b.clientId ) {
|
|
39
|
+
return res.sendError( 'clientId is required', 400 );
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Normalize recipients: trim, drop blanks, de-dupe.
|
|
43
|
+
const emails = Array.isArray( b.reminderEmails ) ? b.reminderEmails : [];
|
|
44
|
+
const reminderEmails = [ ...new Set(
|
|
45
|
+
emails.map( ( e ) => String( e || '' ).trim() ).filter( Boolean ),
|
|
46
|
+
) ];
|
|
47
|
+
|
|
48
|
+
const t = b.templates || {};
|
|
49
|
+
const bool = ( v, d ) => ( typeof v === 'boolean' ? v : d );
|
|
50
|
+
let daysBefore = Number( t.preDue?.daysBefore );
|
|
51
|
+
if ( !Number.isFinite( daysBefore ) ) {
|
|
52
|
+
daysBefore = 3;
|
|
53
|
+
}
|
|
54
|
+
daysBefore = Math.min( 365, Math.max( 1, Math.round( daysBefore ) ) );
|
|
55
|
+
|
|
56
|
+
const templates = {
|
|
57
|
+
preDue: { enabled: bool( t.preDue?.enabled, true ), daysBefore },
|
|
58
|
+
onDue: { enabled: bool( t.onDue?.enabled, true ) },
|
|
59
|
+
onHold: { enabled: bool( t.onHold?.enabled, true ) },
|
|
60
|
+
suspend: { enabled: bool( t.suspend?.enabled, true ) },
|
|
61
|
+
deactivated: { enabled: bool( t.deactivated?.enabled, false ) },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await paymentReminderService.upsert(
|
|
65
|
+
{ clientId: b.clientId },
|
|
66
|
+
{
|
|
67
|
+
clientId: b.clientId,
|
|
68
|
+
reminderEmails,
|
|
69
|
+
templates,
|
|
70
|
+
updatedBy: req.user?.email || req.user?.userName || '',
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const saved = await paymentReminderService.findOne( { clientId: b.clientId } );
|
|
75
|
+
logger.info?.( { function: 'savePaymentReminder', clientId: b.clientId } );
|
|
76
|
+
return res.sendSuccess( saved );
|
|
77
|
+
} catch ( error ) {
|
|
78
|
+
logger.error( { error: error, function: 'savePaymentReminder' } );
|
|
79
|
+
return res.sendError( error, 500 );
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,6 +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 { getPaymentReminder, savePaymentReminder } from '../controllers/paymentReminder.controller.js';
|
|
4
5
|
import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup, gstinLookup } from '../controllers/billing.controllers.js';
|
|
5
6
|
import { billingGroupSchema, clientProductsValid, createBillingGroupsSchema, deleteBillingGroupsSchema, getBillingGroupsSchema, getInvoiceSchema, leadProductsValid, onetimeFeeValid, subscribedStoreListSchema, updateBillingGroupsSchema } from '../dtos/validation.dtos.js';
|
|
6
7
|
|
|
@@ -28,3 +29,7 @@ billingRouter.get( '/getClientProducts/:id', isAllowedSessionHandler, validate(
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
billingRouter.get( '/gst-lookup/:gstin', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), gstinLookup );
|
|
32
|
+
|
|
33
|
+
// Payment reminder config (Billing Settings page), one document per brand.
|
|
34
|
+
billingRouter.get( '/payment-reminder/:clientId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), getPaymentReminder );
|
|
35
|
+
billingRouter.post( '/payment-reminder', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), savePaymentReminder );
|
|
@@ -14,4 +14,5 @@ brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandle
|
|
|
14
14
|
brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getClientBillingInfo );
|
|
15
15
|
brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
|
|
16
16
|
brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpdateBillingGroups );
|
|
17
|
+
brandsBillingRouter.post( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
|
|
17
18
|
brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import model from 'tango-api-schema';
|
|
2
|
+
|
|
3
|
+
export const findOne = async ( query = {}, projection = {} ) => {
|
|
4
|
+
return await model.paymentReminderModel.findOne( query, projection );
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const upsert = async ( filter, update ) => {
|
|
8
|
+
return await model.paymentReminderModel.updateOne( filter, { $set: update }, { upsert: true } );
|
|
9
|
+
};
|