tango-app-api-payment-subscription 3.5.8 → 3.5.9
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 +29 -5
- package/src/controllers/estimate.controller.js +141 -68
- package/src/controllers/invoice.controller.js +27 -3
- package/src/hbs/estimateEmail.hbs +78 -0
- package/src/hbs/estimatePdf.hbs +1632 -118
- package/src/hbs/invoicePdf.hbs +37 -105
- package/src/routes/invoice.routes.js +4 -2
- package/src/services/estimate.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.9",
|
|
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.28",
|
|
33
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
34
34
|
"winston": "^3.12.0",
|
|
35
35
|
"winston-daily-rotate-file": "^5.0.0",
|
|
@@ -173,6 +173,21 @@ export async function brandsBillingList( req, res ) {
|
|
|
173
173
|
dueInr: { $ifNull: [ '$invoiceData.dueInr', 0 ] },
|
|
174
174
|
dueUsd: { $ifNull: [ '$invoiceData.dueUsd', 0 ] },
|
|
175
175
|
nextBillingDate: { $ifNull: [ '$billingData.nextBillingDate', null ] },
|
|
176
|
+
// Whether billing is SET UP — true when a billing group exists (or
|
|
177
|
+
// live products are configured). Drives the Setup-Billing vs View
|
|
178
|
+
// button; must NOT depend on the volatile current-month run count.
|
|
179
|
+
billingConfigured: {
|
|
180
|
+
$or: [
|
|
181
|
+
{ $ne: [ '$billingData', null ] },
|
|
182
|
+
{ $gt: [ {
|
|
183
|
+
$size: { $filter: {
|
|
184
|
+
input: { $ifNull: [ '$planDetails.product', [] ] },
|
|
185
|
+
as: 'prod',
|
|
186
|
+
cond: { $eq: [ '$$prod.status', 'live' ] },
|
|
187
|
+
} },
|
|
188
|
+
}, 0 ] },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
176
191
|
},
|
|
177
192
|
},
|
|
178
193
|
{
|
|
@@ -184,6 +199,7 @@ export async function brandsBillingList( req, res ) {
|
|
|
184
199
|
totalStores: 1,
|
|
185
200
|
billingStores: 1,
|
|
186
201
|
productsAdded: 1,
|
|
202
|
+
billingConfigured: 1,
|
|
187
203
|
dueInr: 1,
|
|
188
204
|
dueUsd: 1,
|
|
189
205
|
status: 1,
|
|
@@ -1561,14 +1577,22 @@ export async function billingSummary( req, res ) {
|
|
|
1561
1577
|
// email's local part since the collection carries no display name.
|
|
1562
1578
|
const usdRate = await getUsdInrRate();
|
|
1563
1579
|
|
|
1564
|
-
// Current month's store count comes from dailyPricing
|
|
1565
|
-
//
|
|
1580
|
+
// Current month's store count comes from dailyPricing — counted the same
|
|
1581
|
+
// way as Brands & Billing's "Billing Stores": distinct ACTIVE stores that
|
|
1582
|
+
// RAN on more than one day in the month (a single-day appearance is
|
|
1583
|
+
// transient and isn't billed). Invoices for the running month usually don't
|
|
1584
|
+
// exist yet, so this is the current-month source.
|
|
1566
1585
|
const curMonthStart = new Date( now.startOf( 'month' ).toISOString() );
|
|
1567
1586
|
const latestDp = await dailyPriceService.aggregate( [
|
|
1568
1587
|
{ $match: { dateISO: { $gte: curMonthStart } } },
|
|
1569
|
-
{ $
|
|
1570
|
-
{ $
|
|
1571
|
-
{ $group: {
|
|
1588
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
1589
|
+
{ $match: { 'stores.status': 'active' } },
|
|
1590
|
+
{ $group: {
|
|
1591
|
+
_id: { clientId: '$clientId', storeId: '$stores.storeId' },
|
|
1592
|
+
days: { $addToSet: { $dateToString: { format: '%Y-%m-%d', date: '$dateISO' } } },
|
|
1593
|
+
} },
|
|
1594
|
+
{ $match: { $expr: { $gt: [ { $size: '$days' }, 1 ] } } },
|
|
1595
|
+
{ $group: { _id: '$_id.clientId', stores: { $sum: 1 } } },
|
|
1572
1596
|
] );
|
|
1573
1597
|
const curStoresByClient = new Map( latestDp.map( ( d ) => [ String( d._id ), d.stores || 0 ] ) );
|
|
1574
1598
|
|
|
@@ -1,12 +1,77 @@
|
|
|
1
1
|
import * as estimateService from '../services/estimate.service.js';
|
|
2
2
|
import * as clientService from '../services/clientPayment.services.js';
|
|
3
|
+
import * as billingService from '../services/billing.service.js';
|
|
3
4
|
import dayjs from 'dayjs';
|
|
4
|
-
import { logger, download } from 'tango-app-api-middleware';
|
|
5
|
+
import { logger, download, sendEmailWithSES } from 'tango-app-api-middleware';
|
|
5
6
|
import Handlebars from '../utils/validations/helper/handlebar.helper.js';
|
|
6
7
|
import fs from 'fs';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import htmlpdf from 'html-pdf-node';
|
|
9
10
|
import { symbolFor } from '../utils/currency.js';
|
|
11
|
+
import { getInvoiceCcEmails } from './invoice.controller.js';
|
|
12
|
+
|
|
13
|
+
// Build the estimate PDF buffer + the template data + a safe filename. Shared
|
|
14
|
+
// by the download and send flows so both render the identical document.
|
|
15
|
+
async function buildEstimatePdf( estimate ) {
|
|
16
|
+
const e = estimate._doc || estimate;
|
|
17
|
+
const currencyType = symbolFor( e.currency );
|
|
18
|
+
const fmt = ( n ) => Number( n || 0 ).toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
19
|
+
|
|
20
|
+
const products = ( e.products || [] ).map( ( p, i ) => {
|
|
21
|
+
let name = String( p.productName || '' ).replace( /([a-z])([A-Z])/g, '$1 $2' );
|
|
22
|
+
name = name.charAt( 0 ).toUpperCase() + name.slice( 1 );
|
|
23
|
+
return {
|
|
24
|
+
index: i + 1,
|
|
25
|
+
productName: name,
|
|
26
|
+
description: p.description || '',
|
|
27
|
+
hsn: p.hsn || p.hsnCode || '998314',
|
|
28
|
+
storeCount: p.storeCount || e.stores || '',
|
|
29
|
+
price: fmt( p.price ),
|
|
30
|
+
amount: fmt( p.amount ),
|
|
31
|
+
};
|
|
32
|
+
} );
|
|
33
|
+
const tax = ( e.tax || [] ).map( ( t ) => ( {
|
|
34
|
+
type: t.type || t.taxName || t.name || 'GST',
|
|
35
|
+
value: t.value ?? t.taxPercentage ?? t.percentage ?? '',
|
|
36
|
+
taxAmount: fmt( t.taxAmount ),
|
|
37
|
+
} ) );
|
|
38
|
+
|
|
39
|
+
const statusLabelMap = { pending: 'Pending', sent: 'Sent', accepted: 'Accepted', declined: 'Declined', expired: 'Expired' };
|
|
40
|
+
const data = {
|
|
41
|
+
estimate: e.estimate,
|
|
42
|
+
status: e.status,
|
|
43
|
+
statusLabel: statusLabelMap[e.status] || e.status,
|
|
44
|
+
companyName: e.companyName || '',
|
|
45
|
+
companyAddress: e.companyAddress || '',
|
|
46
|
+
GSTNumber: e.GSTNumber || '',
|
|
47
|
+
PlaceOfSupply: e.PlaceOfSupply || '',
|
|
48
|
+
groupName: e.groupName || '',
|
|
49
|
+
period: e.period || '',
|
|
50
|
+
createdDate: e.createdDate ? dayjs( e.createdDate ).format( 'DD/MM/YYYY' ) : '',
|
|
51
|
+
validTill: e.validTill ? dayjs( e.validTill ).format( 'DD/MM/YYYY' ) : '',
|
|
52
|
+
currencyType,
|
|
53
|
+
amount: fmt( e.amount ),
|
|
54
|
+
totalAmount: fmt( e.totalAmount ),
|
|
55
|
+
products,
|
|
56
|
+
tax,
|
|
57
|
+
notes: e.notes || '',
|
|
58
|
+
logo: `${JSON.parse( process.env.URL ).apiDomain}/logo.png`,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/estimatePdf.hbs', 'utf8' );
|
|
62
|
+
const html = Handlebars.compile( templateHtml )( data );
|
|
63
|
+
const options = {
|
|
64
|
+
executablePath: '/usr/bin/chromium',
|
|
65
|
+
args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--no-zygote', '--single-process' ],
|
|
66
|
+
format: 'A4',
|
|
67
|
+
margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' },
|
|
68
|
+
printBackground: true,
|
|
69
|
+
preferCSSPageSize: true,
|
|
70
|
+
};
|
|
71
|
+
const pdfBuffer = await htmlpdf.generatePdf( { content: html }, options );
|
|
72
|
+
const filename = ( e.estimate + '-' + ( e.companyName || 'estimate' ) + '.pdf' ).split( '/' ).join( '_' ).split( '"' ).join( '' ).trim();
|
|
73
|
+
return { pdfBuffer, filename, data };
|
|
74
|
+
}
|
|
10
75
|
|
|
11
76
|
// ---------------------------------------------------------------------------
|
|
12
77
|
// Estimates (quotations). A lightweight pre-invoice document with its own
|
|
@@ -43,7 +108,7 @@ async function nextEstimateNumber() {
|
|
|
43
108
|
// flips to 'expired' so the list reflects reality without a cron.
|
|
44
109
|
async function expireOverdue( clientId ) {
|
|
45
110
|
await estimateService.updateOne(
|
|
46
|
-
{ clientId, status: { $in: [ '
|
|
111
|
+
{ clientId, status: { $in: [ 'pending', 'sent' ] }, validTill: { $lt: new Date() } },
|
|
47
112
|
{ $set: { status: 'expired' } },
|
|
48
113
|
);
|
|
49
114
|
}
|
|
@@ -119,7 +184,7 @@ export async function estimateList( req, res ) {
|
|
|
119
184
|
{ $match: { clientId } },
|
|
120
185
|
{ $group: { _id: '$status', count: { $sum: 1 } } },
|
|
121
186
|
] );
|
|
122
|
-
const counts = {
|
|
187
|
+
const counts = { pending: 0, sent: 0, accepted: 0, declined: 0, expired: 0, total: 0 };
|
|
123
188
|
statusAgg.forEach( ( s ) => {
|
|
124
189
|
if ( counts[s._id] != null ) {
|
|
125
190
|
counts[s._id] = s.count;
|
|
@@ -175,7 +240,7 @@ export async function createEstimate( req, res ) {
|
|
|
175
240
|
amount,
|
|
176
241
|
totalAmount,
|
|
177
242
|
currency: b.currency || ( client?.paymentInvoice?.currencyType === 'dollar' ? 'dollar' : 'inr' ),
|
|
178
|
-
status: b.status === 'sent' ? 'sent' : '
|
|
243
|
+
status: b.status === 'sent' ? 'sent' : 'pending',
|
|
179
244
|
createdDate,
|
|
180
245
|
validTill,
|
|
181
246
|
createdBy: req.user?.email || req.user?.userName || '',
|
|
@@ -240,7 +305,7 @@ export async function updateEstimate( req, res ) {
|
|
|
240
305
|
update.period = b.period;
|
|
241
306
|
}
|
|
242
307
|
// Allow a status nudge (e.g. draft → sent) on save, but never to accepted.
|
|
243
|
-
if ( b.status && [ '
|
|
308
|
+
if ( b.status && [ 'pending', 'sent' ].includes( b.status ) ) {
|
|
244
309
|
update.status = b.status;
|
|
245
310
|
}
|
|
246
311
|
|
|
@@ -270,7 +335,7 @@ export async function getEstimate( req, res ) {
|
|
|
270
335
|
export async function estimateStatusUpdate( req, res ) {
|
|
271
336
|
try {
|
|
272
337
|
const { estimateId, status } = req.body || {};
|
|
273
|
-
const allowed = [ '
|
|
338
|
+
const allowed = [ 'pending', 'sent', 'accepted', 'declined', 'expired' ];
|
|
274
339
|
if ( !estimateId || !allowed.includes( status ) ) {
|
|
275
340
|
return res.sendError( 'estimateId and a valid status are required', 400 );
|
|
276
341
|
}
|
|
@@ -293,66 +358,7 @@ export async function downloadEstimate( req, res ) {
|
|
|
293
358
|
if ( !estimate ) {
|
|
294
359
|
return res.sendError( 'Estimate not found', 404 );
|
|
295
360
|
}
|
|
296
|
-
const
|
|
297
|
-
const currencyType = symbolFor( e.currency );
|
|
298
|
-
const fmt = ( n ) => Number( n || 0 ).toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
|
|
299
|
-
|
|
300
|
-
const products = ( e.products || [] ).map( ( p, i ) => {
|
|
301
|
-
let name = String( p.productName || '' ).replace( /([a-z])([A-Z])/g, '$1 $2' );
|
|
302
|
-
name = name.charAt( 0 ).toUpperCase() + name.slice( 1 );
|
|
303
|
-
return {
|
|
304
|
-
index: i + 1,
|
|
305
|
-
productName: name,
|
|
306
|
-
description: p.description || '',
|
|
307
|
-
hsn: p.hsn || p.hsnCode || '998314',
|
|
308
|
-
storeCount: p.storeCount || e.stores || '',
|
|
309
|
-
price: fmt( p.price ),
|
|
310
|
-
amount: fmt( p.amount ),
|
|
311
|
-
};
|
|
312
|
-
} );
|
|
313
|
-
const tax = ( e.tax || [] ).map( ( t ) => ( {
|
|
314
|
-
type: t.type || t.taxName || t.name || 'GST',
|
|
315
|
-
value: t.value ?? t.taxPercentage ?? t.percentage ?? '',
|
|
316
|
-
taxAmount: fmt( t.taxAmount ),
|
|
317
|
-
} ) );
|
|
318
|
-
|
|
319
|
-
const statusLabelMap = { draft: 'Draft', sent: 'Sent', accepted: 'Accepted', declined: 'Declined', expired: 'Expired' };
|
|
320
|
-
const data = {
|
|
321
|
-
estimate: e.estimate,
|
|
322
|
-
status: e.status,
|
|
323
|
-
statusLabel: statusLabelMap[e.status] || e.status,
|
|
324
|
-
companyName: e.companyName || '',
|
|
325
|
-
companyAddress: e.companyAddress || '',
|
|
326
|
-
GSTNumber: e.GSTNumber || '',
|
|
327
|
-
PlaceOfSupply: e.PlaceOfSupply || '',
|
|
328
|
-
groupName: e.groupName || '',
|
|
329
|
-
period: e.period || '',
|
|
330
|
-
createdDate: e.createdDate ? dayjs( e.createdDate ).format( 'DD/MM/YYYY' ) : '',
|
|
331
|
-
validTill: e.validTill ? dayjs( e.validTill ).format( 'DD/MM/YYYY' ) : '',
|
|
332
|
-
currencyType,
|
|
333
|
-
amount: fmt( e.amount ),
|
|
334
|
-
totalAmount: fmt( e.totalAmount ),
|
|
335
|
-
products,
|
|
336
|
-
tax,
|
|
337
|
-
notes: e.notes || '',
|
|
338
|
-
logo: `${JSON.parse( process.env.URL ).apiDomain}/logo.png`,
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const templateHtml = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/estimatePdf.hbs', 'utf8' );
|
|
342
|
-
const template = Handlebars.compile( templateHtml );
|
|
343
|
-
const html = template( data );
|
|
344
|
-
const file = { content: html };
|
|
345
|
-
const options = {
|
|
346
|
-
executablePath: '/usr/bin/chromium',
|
|
347
|
-
args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--no-zygote', '--single-process' ],
|
|
348
|
-
format: 'A4',
|
|
349
|
-
margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' },
|
|
350
|
-
printBackground: true,
|
|
351
|
-
preferCSSPageSize: true,
|
|
352
|
-
};
|
|
353
|
-
const pdfBuffer = await htmlpdf.generatePdf( file, options );
|
|
354
|
-
|
|
355
|
-
const filename = ( e.estimate + '-' + ( e.companyName || 'estimate' ) + '.pdf' ).split( '/' ).join( '_' ).split( '"' ).join( '' ).trim();
|
|
361
|
+
const { pdfBuffer, filename } = await buildEstimatePdf( estimate );
|
|
356
362
|
res.set( 'Content-Type', 'application/pdf' );
|
|
357
363
|
res.set( 'Content-Disposition', `attachment; filename="${filename}"` );
|
|
358
364
|
return res.send( pdfBuffer );
|
|
@@ -362,6 +368,71 @@ export async function downloadEstimate( req, res ) {
|
|
|
362
368
|
}
|
|
363
369
|
}
|
|
364
370
|
|
|
371
|
+
// Approve & send an estimate: render the PDF, email it (with the PDF attached)
|
|
372
|
+
// to the billing group's recipients — same recipient logic as invoice send —
|
|
373
|
+
// then flip the status to 'sent'. Triggered by the Approve action's confirm.
|
|
374
|
+
export async function sendEstimate( req, res ) {
|
|
375
|
+
try {
|
|
376
|
+
const estimateId = req.body?.estimateId || req.params?.estimateId;
|
|
377
|
+
if ( !estimateId ) {
|
|
378
|
+
return res.sendError( 'estimateId is required', 400 );
|
|
379
|
+
}
|
|
380
|
+
const estimate = await estimateService.findOne( { _id: estimateId } );
|
|
381
|
+
if ( !estimate ) {
|
|
382
|
+
return res.sendError( 'Estimate not found', 404 );
|
|
383
|
+
}
|
|
384
|
+
const e = estimate._doc || estimate;
|
|
385
|
+
if ( e.status === 'accepted' ) {
|
|
386
|
+
return res.sendError( 'An accepted estimate cannot be re-sent.', 409 );
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Recipients: billing group's generateInvoiceTo (TO) + invoice heads /
|
|
390
|
+
// assigned CSMs (CC) — identical to the invoice send flow.
|
|
391
|
+
let toEmails = [];
|
|
392
|
+
if ( e.groupId ) {
|
|
393
|
+
const group = await billingService.findOne( { _id: e.groupId } );
|
|
394
|
+
toEmails = ( group?.generateInvoiceTo || [] ).map( ( x ) => String( x || '' ).trim() ).filter( Boolean );
|
|
395
|
+
}
|
|
396
|
+
toEmails = [ ...new Set( toEmails ) ];
|
|
397
|
+
if ( !toEmails.length ) {
|
|
398
|
+
return res.sendError( 'To Email not Found — configure recipients on the billing group.', 400 );
|
|
399
|
+
}
|
|
400
|
+
const ccRaw = await getInvoiceCcEmails( e.clientId );
|
|
401
|
+
const toSet = new Set( toEmails.map( ( x ) => x.toLowerCase() ) );
|
|
402
|
+
const ccEmails = [ ...new Set( ( ccRaw || [] ).map( ( x ) => String( x || '' ).trim() ).filter( Boolean ) ) ]
|
|
403
|
+
.filter( ( x ) => !toSet.has( x.toLowerCase() ) );
|
|
404
|
+
|
|
405
|
+
const { pdfBuffer, filename, data } = await buildEstimatePdf( estimate );
|
|
406
|
+
|
|
407
|
+
const client = await clientService.findOne( { clientId: e.clientId }, { clientName: 1 } );
|
|
408
|
+
const emailData = {
|
|
409
|
+
...data,
|
|
410
|
+
clientName: client?.clientName || e.companyName || 'Customer',
|
|
411
|
+
// The email is sent FROM Tango, so the sign-off is the sender — not the
|
|
412
|
+
// client's registered company name carried on the estimate.
|
|
413
|
+
companyName: 'Team Tango',
|
|
414
|
+
};
|
|
415
|
+
const emailHtml = Handlebars.compile(
|
|
416
|
+
fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/estimateEmail.hbs', 'utf8' ),
|
|
417
|
+
)( emailData );
|
|
418
|
+
|
|
419
|
+
const SES = JSON.parse( process.env.SES );
|
|
420
|
+
const fromEmail = SES.accountsEmail || SES.adminEmail;
|
|
421
|
+
const subject = `Estimate ${e.estimate} - Tango/${client?.clientName || e.companyName || ''}`;
|
|
422
|
+
const attachment = { filename, content: pdfBuffer, contentType: 'application/pdf' };
|
|
423
|
+
|
|
424
|
+
await sendEmailWithSES( toEmails, subject, emailHtml, attachment, fromEmail, ccEmails.length ? ccEmails : undefined );
|
|
425
|
+
|
|
426
|
+
// Mark sent only after a successful send.
|
|
427
|
+
await estimateService.updateOne( { _id: estimateId }, { $set: { status: 'sent' } } );
|
|
428
|
+
logger.info?.( { function: 'sendEstimate', estimateId, to: toEmails, cc: ccEmails } );
|
|
429
|
+
return res.sendSuccess( { estimateId, status: 'sent', sentTo: toEmails } );
|
|
430
|
+
} catch ( error ) {
|
|
431
|
+
logger.error( { error: error, function: 'sendEstimate' } );
|
|
432
|
+
return res.sendError( error, 500 );
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
365
436
|
export async function deleteEstimate( req, res ) {
|
|
366
437
|
try {
|
|
367
438
|
const estimate = await estimateService.findOne( { _id: req.params.estimateId } );
|
|
@@ -371,8 +442,10 @@ export async function deleteEstimate( req, res ) {
|
|
|
371
442
|
if ( estimate.status === 'accepted' ) {
|
|
372
443
|
return res.sendError( 'An accepted estimate cannot be deleted.', 409 );
|
|
373
444
|
}
|
|
374
|
-
|
|
375
|
-
|
|
445
|
+
// Hard delete — remove the document from the collection (not a soft
|
|
446
|
+
// status change).
|
|
447
|
+
await estimateService.deleteOne( { _id: req.params.estimateId } );
|
|
448
|
+
return res.sendSuccess( 'Estimate deleted' );
|
|
376
449
|
} catch ( error ) {
|
|
377
450
|
logger.error( { error: error, function: 'deleteEstimate' } );
|
|
378
451
|
return res.sendError( error, 500 );
|
|
@@ -24,7 +24,7 @@ import { getUsdInrRate } from './brandsBilling.controller.js';
|
|
|
24
24
|
// userAssignedStore (clientId + tangoUserType:'csm'). Returns a flat de-duped
|
|
25
25
|
// array suitable for the SES CC field. Returns [] on any failure — invoice
|
|
26
26
|
// email send should never fail because of missing settings or DB hiccups.
|
|
27
|
-
async function getInvoiceCcEmails( clientId ) {
|
|
27
|
+
export async function getInvoiceCcEmails( clientId ) {
|
|
28
28
|
const collected = [];
|
|
29
29
|
|
|
30
30
|
// 1. Configured heads (global, applies to every invoice).
|
|
@@ -273,9 +273,15 @@ export async function createInvoice( req, res ) {
|
|
|
273
273
|
|
|
274
274
|
] );
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
// billingDate is the actual invoice (creation) date — even for advance
|
|
277
|
+
// invoices. The advance MONTH is reflected only in the product lines /
|
|
278
|
+
// monthOfbilling (driven by baseDate), not in the billing date itself.
|
|
279
|
+
// (Regenerate keeps the original invoice's billingDate.)
|
|
280
|
+
const creationDate = dayjs();
|
|
281
|
+
let invoicedate = req.body.invoiceId ? dayjs( findInvoice.billingDate ).format( 'YYYY-MM-DD' ) : creationDate.format( 'YYYY-MM-DD' );
|
|
277
282
|
let daysExtend = group?.paymentTerm ? group?.paymentTerm : 30;
|
|
278
|
-
|
|
283
|
+
// Due date is relative to the billing (creation) date.
|
|
284
|
+
let dueDate = creationDate.add( daysExtend, 'days' );
|
|
279
285
|
console.log( 'group.currencygroup.currency', group.currency );
|
|
280
286
|
let data = {
|
|
281
287
|
groupName: group.groupName,
|
|
@@ -842,6 +848,24 @@ export async function invoiceDownloadBulk( req, res ) {
|
|
|
842
848
|
}
|
|
843
849
|
}
|
|
844
850
|
|
|
851
|
+
// Bank/beneficiary details for the invoice preview. Fixed beneficiary account
|
|
852
|
+
// — hardcoded to match the PDF exactly (NOT read from the DB).
|
|
853
|
+
export async function invoiceBankDetails( req, res ) {
|
|
854
|
+
try {
|
|
855
|
+
return res.sendSuccess( {
|
|
856
|
+
beneficiaryName: 'Tango IT Solutions India Private Limited',
|
|
857
|
+
accountNumber: '50200027441433',
|
|
858
|
+
ifsc: 'HDFC0000386',
|
|
859
|
+
swift: 'HDFCINBBCHE',
|
|
860
|
+
branch: 'Santhome, Chennai',
|
|
861
|
+
paymentType: 'Online',
|
|
862
|
+
} );
|
|
863
|
+
} catch ( error ) {
|
|
864
|
+
logger.error( { error: error, function: 'invoiceBankDetails', invoiceId: req.params.invoiceId } );
|
|
865
|
+
return res.sendError( error, 500 );
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
845
869
|
export async function invoiceAnnexure( req, res ) {
|
|
846
870
|
try {
|
|
847
871
|
const invoiceInfo = await invoiceService.findOne( { _id: req.params.invoiceId } );
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Estimate</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body style="margin:0;padding:0;background-color:#dbe5ea;font-family:'Inter',Arial,sans-serif;">
|
|
9
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding:0 10px;">
|
|
10
|
+
<tr>
|
|
11
|
+
<td style="padding:32px 10px 0 10px;">
|
|
12
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:680px;margin:0 auto;" align="center">
|
|
13
|
+
<tr>
|
|
14
|
+
<td style="background-color:#ffffff;padding:24px 24px 0 18px;">
|
|
15
|
+
<img src="{{logo}}" width="200" height="100" alt="Tango Eye" style="vertical-align:middle;border:0;height:auto;">
|
|
16
|
+
</td>
|
|
17
|
+
</tr>
|
|
18
|
+
<tr>
|
|
19
|
+
<td style="background-color:#ffffff;padding:0 24px;">
|
|
20
|
+
<p style="border-top:1px solid #CBD5E1;margin:0;"></p>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
<tr>
|
|
24
|
+
<td style="background-color:#ffffff;padding:18px 24px 6px 30px;">
|
|
25
|
+
<span style="font-weight:700;color:#121A26;font-size:22px;line-height:32px;">Your estimate {{estimate}}</span>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
<tr>
|
|
29
|
+
<td style="background-color:#ffffff;padding:14px 24px 4px 30px;font-size:16px;line-height:150%;color:#384860;">
|
|
30
|
+
<p style="margin:0;">Hi {{clientName}},</p>
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<tr>
|
|
34
|
+
<td style="background-color:#ffffff;padding:10px 24px 6px 30px;font-size:16px;line-height:150%;color:#384860;">
|
|
35
|
+
<p style="margin:0;">Please find attached the estimate <strong>{{estimate}}</strong>{{#if period}} for <strong>{{period}}</strong>{{/if}}. A summary is shown below; the attached PDF has the full breakdown.</p>
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
|
|
39
|
+
<tr>
|
|
40
|
+
<td style="background-color:#ffffff;padding:14px 24px 6px 30px;">
|
|
41
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border:1px solid #E2E8F0;border-radius:8px;border-collapse:separate;overflow:hidden;">
|
|
42
|
+
<tr style="background-color:#F1F5F9;">
|
|
43
|
+
<td style="padding:10px 14px;font-size:13px;color:#64748B;font-weight:600;">Estimate Number</td>
|
|
44
|
+
<td style="padding:10px 14px;font-size:13px;color:#121A26;font-weight:700;text-align:right;">{{estimate}}</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<tr>
|
|
47
|
+
<td style="padding:10px 14px;font-size:13px;color:#64748B;font-weight:600;border-top:1px solid #E2E8F0;">Total Amount</td>
|
|
48
|
+
<td style="padding:10px 14px;font-size:13px;color:#121A26;font-weight:700;text-align:right;border-top:1px solid #E2E8F0;">{{currencyType}} {{totalAmount}}</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td style="padding:10px 14px;font-size:13px;color:#64748B;font-weight:600;border-top:1px solid #E2E8F0;">Valid Till</td>
|
|
52
|
+
<td style="padding:10px 14px;font-size:13px;color:#121A26;font-weight:700;text-align:right;border-top:1px solid #E2E8F0;">{{validTill}}</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</table>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
|
|
58
|
+
<tr>
|
|
59
|
+
<td style="background-color:#ffffff;padding:14px 24px 6px 30px;font-size:16px;line-height:150%;color:#384860;">
|
|
60
|
+
<p style="margin:0;">This is an estimate, not a tax invoice. Prices are subject to the terms agreed in the final subscription. If you'd like to proceed or have any questions, please reply to this email.</p>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<td style="background-color:#ffffff;padding:14px 24px 24px 30px;font-size:16px;line-height:150%;color:#384860;">
|
|
65
|
+
<p style="margin:0;">Best regards,<br>{{companyName}}</p>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
<tr>
|
|
69
|
+
<td style="background-color:#ffffff;padding:10px 24px 18px 30px;font-size:12px;color:#202B3C;line-height:150%;">
|
|
70
|
+
<p style="margin:0;">© Tango Eye. All rights reserved.</p>
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
</table>
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</table>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|