tango-app-api-payment-subscription 3.5.2 → 3.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import * as invoiceService from '../services/invoice.service.js';
|
|
|
2
2
|
import * as dailyPricingService from '../services/dailyPrice.service.js';
|
|
3
3
|
import * as clientService from '../services/clientPayment.services.js';
|
|
4
4
|
import * as billingService from '../services/billing.service.js';
|
|
5
|
+
import mongoose from 'mongoose';
|
|
5
6
|
import dayjs from 'dayjs';
|
|
6
7
|
import { logger, checkFileExist, signedUrl, download, sendEmailWithSES, insertOpenSearchData } from 'tango-app-api-middleware';
|
|
7
8
|
// import Handlebars from 'handlebars';
|
|
@@ -65,6 +66,25 @@ async function getInvoiceCcEmails( clientId ) {
|
|
|
65
66
|
export async function createInvoice( req, res ) {
|
|
66
67
|
try {
|
|
67
68
|
let invoiceGroupList = [];
|
|
69
|
+
// Optional groupIds filter — when present, only those billing groups will
|
|
70
|
+
// be processed. Lets the UI generate an invoice for a specific subset of
|
|
71
|
+
// groups instead of every group on the client. Strings are matched against
|
|
72
|
+
// billing _id.
|
|
73
|
+
const groupIdsFilter = Array.isArray( req.body.groupIds ) && req.body.groupIds.length > 0 ?
|
|
74
|
+
req.body.groupIds :
|
|
75
|
+
null;
|
|
76
|
+
const groupObjectIdFilter = groupIdsFilter ?
|
|
77
|
+
groupIdsFilter
|
|
78
|
+
.map( ( id ) => {
|
|
79
|
+
try {
|
|
80
|
+
return new mongoose.Types.ObjectId( id );
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
} )
|
|
85
|
+
.filter( ( id ) => id !== null ) :
|
|
86
|
+
null;
|
|
87
|
+
|
|
68
88
|
if ( req.body.allClient ) {
|
|
69
89
|
if ( req.body.clientList && req.body.clientList.length > 0 ) {
|
|
70
90
|
req.body.clientList = req.body.clientList;
|
|
@@ -74,12 +94,12 @@ export async function createInvoice( req, res ) {
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
for ( let client of req.body.clientList ) {
|
|
97
|
+
const matchStage = { clientId: client };
|
|
98
|
+
if ( groupObjectIdFilter && groupObjectIdFilter.length ) {
|
|
99
|
+
matchStage._id = { $in: groupObjectIdFilter };
|
|
100
|
+
}
|
|
77
101
|
let invoiceGroup = await billingService.aggregatebilling( [
|
|
78
|
-
{
|
|
79
|
-
$match: {
|
|
80
|
-
clientId: client,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
102
|
+
{ $match: matchStage },
|
|
83
103
|
] );
|
|
84
104
|
for ( let invGrp of invoiceGroup ) {
|
|
85
105
|
invoiceGroupList.push( invGrp );
|
|
@@ -93,9 +113,82 @@ export async function createInvoice( req, res ) {
|
|
|
93
113
|
invoiceGroupList.push( invoiceGroup );
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
// Custom-create path: the Create Invoice modal posts a fully-composed
|
|
117
|
+
// invoice (companyName, products, tax, totals — all entered by the user).
|
|
118
|
+
// The legacy aggregation paths above don't handle this shape and would
|
|
119
|
+
// silently no-op (empty invoiceGroupList → loop skipped → 2xx with nothing
|
|
120
|
+
// written). When the caller sets customInvoice=true we insert exactly
|
|
121
|
+
// what they sent, with a server-generated invoice number.
|
|
122
|
+
if ( req.body.customInvoice === true ) {
|
|
123
|
+
if ( !req.body.clientId ) {
|
|
124
|
+
return res.sendError( 'clientId is required for customInvoice', 400 );
|
|
125
|
+
}
|
|
126
|
+
const Finacialyear = getCurrentFinancialYear();
|
|
127
|
+
const previousinvoice = await invoiceService.findandsort(
|
|
128
|
+
{ invoice: { $regex: `^INV-${Finacialyear}-` } },
|
|
129
|
+
{},
|
|
130
|
+
{ invoiceIndex: -1 },
|
|
131
|
+
);
|
|
132
|
+
let invoiceNo = '00001';
|
|
133
|
+
if ( previousinvoice && previousinvoice.length > 0 ) {
|
|
134
|
+
invoiceNo = Number( previousinvoice[0].invoiceIndex ) + 1;
|
|
135
|
+
invoiceNo = invoiceNo.toString().padStart( 5, '0' );
|
|
136
|
+
}
|
|
137
|
+
const baseDate = req.body.billingDate ? dayjs( req.body.billingDate ) : dayjs();
|
|
138
|
+
const data = {
|
|
139
|
+
invoice: `INV-${Finacialyear}-${invoiceNo}`,
|
|
140
|
+
invoiceIndex: invoiceNo,
|
|
141
|
+
clientId: req.body.clientId,
|
|
142
|
+
groupId: req.body.groupId || undefined,
|
|
143
|
+
groupName: req.body.groupName || '',
|
|
144
|
+
companyName: req.body.companyName || '',
|
|
145
|
+
companyAddress: req.body.companyAddress || '',
|
|
146
|
+
GSTNumber: req.body.GSTNumber || '',
|
|
147
|
+
PlaceOfSupply: req.body.PlaceOfSupply || '',
|
|
148
|
+
products: Array.isArray( req.body.products ) ? req.body.products : [],
|
|
149
|
+
tax: Array.isArray( req.body.tax ) ? req.body.tax : [],
|
|
150
|
+
amount: Math.round( Number( req.body.amount ) || 0 ),
|
|
151
|
+
totalAmount: Math.round( Number( req.body.totalAmount ) || 0 ),
|
|
152
|
+
stores: Number( req.body.stores ) || 0,
|
|
153
|
+
currency: req.body.currency || 'inr',
|
|
154
|
+
billingDate: baseDate.toDate(),
|
|
155
|
+
dueDate: req.body.dueDate ? new Date( req.body.dueDate ) : baseDate.add( 30, 'days' ).toDate(),
|
|
156
|
+
monthOfbilling: baseDate.format( 'MM' ),
|
|
157
|
+
paymentMethod: 'Online',
|
|
158
|
+
status: 'pendingCsm',
|
|
159
|
+
paymentStatus: 'unpaid',
|
|
160
|
+
};
|
|
161
|
+
const created = await invoiceService.create( data );
|
|
162
|
+
const logObj = {
|
|
163
|
+
userName: req.user?.userName,
|
|
164
|
+
email: req.user?.email,
|
|
165
|
+
clientId: data.clientId,
|
|
166
|
+
logSubType: 'invoiceCreated',
|
|
167
|
+
logType: 'invoice',
|
|
168
|
+
date: new Date(),
|
|
169
|
+
changes: [ `${data.invoice} invoice created manually by ${req.user?.email}` ],
|
|
170
|
+
eventType: 'create',
|
|
171
|
+
timestamp: new Date(),
|
|
172
|
+
showTo: [ 'tango' ],
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
|
|
176
|
+
} catch ( logErr ) {
|
|
177
|
+
logger.error( { error: logErr, function: 'createInvoice.customLog' } );
|
|
178
|
+
}
|
|
179
|
+
return res.sendSuccess( { data: created } );
|
|
180
|
+
}
|
|
181
|
+
|
|
96
182
|
for ( let group of invoiceGroupList ) {
|
|
97
183
|
let Finacialyear = getCurrentFinancialYear();
|
|
98
|
-
|
|
184
|
+
// Scope the highest-index lookup to invoices created in the current FY
|
|
185
|
+
// (invoice IDs are `INV-${FY}-${index}`). Without this scope the new FY
|
|
186
|
+
// would continue the previous year's sequence instead of resetting.
|
|
187
|
+
let previousinvoice = await invoiceService.findandsort(
|
|
188
|
+
{ invoice: { $regex: `^INV-${Finacialyear}-` } },
|
|
189
|
+
{},
|
|
190
|
+
{ invoiceIndex: -1 },
|
|
191
|
+
);
|
|
99
192
|
let invoiceNo = '00001';
|
|
100
193
|
if ( previousinvoice && previousinvoice.length > 0 ) {
|
|
101
194
|
invoiceNo = Number( previousinvoice[0].invoiceIndex ) + 1;
|
|
@@ -2110,6 +2203,13 @@ export async function updateInvoice( req, res ) {
|
|
|
2110
2203
|
return res.sendError( 'Invoice not found', 404 );
|
|
2111
2204
|
}
|
|
2112
2205
|
|
|
2206
|
+
// Lock once final-approved. The UI hides the Edit icon for these rows,
|
|
2207
|
+
// but a direct API call would otherwise still let someone mutate a
|
|
2208
|
+
// finalised invoice. Match the frontend's isInvoiceLocked() check.
|
|
2209
|
+
if ( invoice.status === 'approved' ) {
|
|
2210
|
+
return res.sendError( 'Cannot edit a final-approved invoice.', 409 );
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2113
2213
|
let updateData = {};
|
|
2114
2214
|
const allowedFields = [
|
|
2115
2215
|
'companyName', 'companyAddress', 'GSTNumber', 'PlaceOfSupply',
|
|
@@ -2220,6 +2320,12 @@ export async function deleteInvoice( req, res ) {
|
|
|
2220
2320
|
return res.sendError( 'Invoice not found', 404 );
|
|
2221
2321
|
}
|
|
2222
2322
|
|
|
2323
|
+
// Same lock the UI enforces — a final-approved invoice must not be
|
|
2324
|
+
// deletable, even by a power user hitting the API directly.
|
|
2325
|
+
if ( invoice.status === 'approved' ) {
|
|
2326
|
+
return res.sendError( 'Cannot delete a final-approved invoice.', 409 );
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2223
2329
|
await invoiceService.deleteRecord( { _id: invoiceId } );
|
|
2224
2330
|
|
|
2225
2331
|
const logObj = {
|
|
@@ -3772,7 +3772,15 @@ export const invoiceGenerate = async ( req, res ) => {
|
|
|
3772
3772
|
}
|
|
3773
3773
|
|
|
3774
3774
|
|
|
3775
|
-
|
|
3775
|
+
// Scope the highest-index lookup to the current financial year
|
|
3776
|
+
// (invoice IDs are `INV-${FY}-${index}`). Sort by invoiceIndex (not
|
|
3777
|
+
// _id) so the largest sequence number wins even if records were
|
|
3778
|
+
// back-dated or imported out of order.
|
|
3779
|
+
let previousinvoice = await invoiceService.findandsort(
|
|
3780
|
+
{ invoice: { $regex: `^INV-${Finacialyear}-` } },
|
|
3781
|
+
{},
|
|
3782
|
+
{ invoiceIndex: -1 },
|
|
3783
|
+
);
|
|
3776
3784
|
let invoiceNo = '00001';
|
|
3777
3785
|
if ( previousinvoice && previousinvoice.length > 0 ) {
|
|
3778
3786
|
invoiceNo = Number( previousinvoice[0].invoiceIndex ) + 1;
|
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -1643,7 +1643,7 @@
|
|
|
1643
1643
|
</div>
|
|
1644
1644
|
<div class="frame-54">
|
|
1645
1645
|
<div class="ifsc-code">IFSC Code</div>
|
|
1646
|
-
<div class="hdfc-0000269">
|
|
1646
|
+
<div class="hdfc-0000269">HDFC0000386</div>
|
|
1647
1647
|
</div>
|
|
1648
1648
|
<div class="frame-532">
|
|
1649
1649
|
<div class="payment-type">Payment Type</div>
|