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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-payment-subscription",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "paymentSubscription",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
- let previousinvoice = await invoiceService.findandsort( {}, {}, { invoiceIndex: -1 } );
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
- let previousinvoice = await invoiceService.findandsort( {}, {}, { _id: -1 } );
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;
@@ -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">HDFC0000269</div>
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>