tango-app-api-payment-subscription 3.5.16 → 3.5.18

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.16",
3
+ "version": "3.5.18",
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.34",
32
+ "tango-api-schema": "^2.6.36",
33
33
  "tango-app-api-middleware": "^3.6.18",
34
34
  "winston": "^3.12.0",
35
35
  "winston-daily-rotate-file": "^5.0.0",
@@ -36,7 +36,7 @@ import mongoose from 'mongoose';
36
36
  import 'dotenv/config';
37
37
  import { getConnection } from '../config/database/database.js';
38
38
 
39
- const DEFAULT_CLIENTS = [ '387', '193' ];
39
+ const DEFAULT_CLIENTS = [ '387', '193', '11' ];
40
40
 
41
41
  // Fallback billing group for stores that have no storeProfile.country.
42
42
  const UNKNOWN_GROUP_NAME = 'Unknown';
@@ -266,7 +266,37 @@ export const getAllBillingGroups = async ( req, res ) => {
266
266
  try {
267
267
  const billingGroups = await find( { clientId: req.query.clientId } );
268
268
 
269
- return res.sendSuccess( billingGroups );
269
+ // Per group: does an ADVANCE invoice already cover the CURRENT month? If so,
270
+ // attach { invoice, period } so the Generate Invoice popup can show it (and
271
+ // signal a normal monthly invoice will be made instead of another advance).
272
+ const currentMonthLabel = dayjs().format( 'MMM YYYY' );
273
+ const enriched = await Promise.all( ( billingGroups || [] ).map( async ( g ) => {
274
+ const plain = g.toObject?.() || g;
275
+ let advanceCoverage = null;
276
+ try {
277
+ const adv = await invoiceService.findOne( {
278
+ 'groupId': g._id,
279
+ 'advanceInvoice': true,
280
+ 'products.month': currentMonthLabel,
281
+ } );
282
+ if ( adv ) {
283
+ // Ordered distinct month labels across the advance invoice's line
284
+ // items → period "first to last" (e.g. "Jun-2026 to Aug-2026").
285
+ const months = [ ...new Set( ( adv.products || [] ).map( ( p ) => p.month ).filter( Boolean ) ) ];
286
+ months.sort( ( a, b ) => dayjs( a, 'MMM YYYY' ).valueOf() - dayjs( b, 'MMM YYYY' ).valueOf() );
287
+ const fmt = ( m ) => dayjs( m, 'MMM YYYY' ).format( 'MMM-YYYY' );
288
+ const period = months.length ?
289
+ ( months.length > 1 ? `${fmt( months[0] )} to ${fmt( months[months.length - 1] )}` : fmt( months[0] ) ) :
290
+ '';
291
+ advanceCoverage = { invoice: adv.invoice, period };
292
+ }
293
+ } catch ( e ) {
294
+ // Coverage is advisory only — never fail the group list over it.
295
+ }
296
+ return { ...plain, advanceCoverage };
297
+ } ) );
298
+
299
+ return res.sendSuccess( enriched );
270
300
  } catch ( error ) {
271
301
  logger.error( { error: error, function: 'getBillingGroups' } );
272
302
  return res.sendError( error, 500 );
@@ -1057,8 +1057,11 @@ export async function updateDailyPricingWorkingDays( req, res ) {
1057
1057
  { arrayFilters: [ { 'store.storeId': storeId }, { 'product.productName': productName } ] },
1058
1058
  );
1059
1059
 
1060
- if ( result.modifiedCount === 0 ) {
1061
- return res.sendError( 'Record not found or no changes made', 204 );
1060
+ // Only a missing record is a real failure. matchedCount > 0 with
1061
+ // modifiedCount 0 just means the value was already that number (e.g. saving
1062
+ // 0 on an already-0 row) — treat that as success, not an error.
1063
+ if ( ( result.matchedCount || 0 ) === 0 ) {
1064
+ return res.sendError( 'Record not found', 404 );
1062
1065
  }
1063
1066
 
1064
1067
  const logObj = {
@@ -2270,3 +2273,330 @@ export async function additionalProducts( req, res ) {
2270
2273
  return res.sendError( error, 500 );
2271
2274
  }
2272
2275
  }
2276
+
2277
+ // POST a JSON payload to a Lambda Function URL and return the parsed JSON
2278
+ // (or false on failure). Node's fetch forbids a body on GET, so POST is used.
2279
+ async function lamdaServiceCall( url, data ) {
2280
+ try {
2281
+ const response = await fetch( url, {
2282
+ method: 'POST',
2283
+ headers: { 'Content-Type': 'application/json' },
2284
+ body: JSON.stringify( data ),
2285
+ signal: AbortSignal.timeout( 300000 ),
2286
+ } );
2287
+ if ( !response.ok ) {
2288
+ throw new Error( `Response status: ${response.status}` );
2289
+ }
2290
+ return await response.json();
2291
+ } catch ( error ) {
2292
+ logger.error( { error: error, message: data, function: 'lamdaServiceCall' } );
2293
+ return false;
2294
+ }
2295
+ }
2296
+
2297
+ // Unit price for a product from the latest billing_details OpenSearch record.
2298
+ // productName match is case-insensitive. Returns 0 if not found / unavailable.
2299
+ async function billingDetailsPrice( clientId, productName ) {
2300
+ try {
2301
+ const osRes = await getOpenSearchData( BILLING_DETAILS_INDEX, {
2302
+ size: 1,
2303
+ query: { term: { 'client_id': clientId } },
2304
+ sort: [ { date_string: { order: 'desc' } } ],
2305
+ } );
2306
+ const hits = osRes?.body?.hits?.hits || osRes?.hits?.hits || [];
2307
+ const match = ( hits[0]?._source?.products || [] )
2308
+ .find( ( p ) => String( p.productName ).toLowerCase() === String( productName ).toLowerCase() );
2309
+ return Number( match?.price ) || 0;
2310
+ } catch ( osErr ) {
2311
+ logger.error( { error: osErr, function: 'billingDetailsPrice', clientId, productName } );
2312
+ return 0;
2313
+ }
2314
+ }
2315
+
2316
+ // Build the Lambda-backed export rows (VMS / Run AI / etc.). Calls the product's
2317
+ // Lambda for store_names (storeIds), maps them to the stores collection for
2318
+ // name + country (Zone), with current-month days and the billing_details price.
2319
+ async function lambdaStoreExport( clientId, lambdaUrl, billingProductName ) {
2320
+ const toDate = dayjs().format( 'DD-MM-YYYY' );
2321
+ const lambdaResult = await lamdaServiceCall( lambdaUrl, { to_date: toDate, client_id: clientId } );
2322
+ console.log( '🚀 ~ lambdaStoreExport ~ lambdaResult:', lambdaResult );
2323
+ // These Lambdas return the storeIds under either `store_ids` (Run AI) or
2324
+ // `store_names` (VMS) with no status_code. Accept whichever is present; data
2325
+ // availability = a non-empty array.
2326
+ const rawIds = ( lambdaResult && Array.isArray( lambdaResult.store_ids ) ) ? lambdaResult.store_ids :
2327
+ ( ( lambdaResult && Array.isArray( lambdaResult.store_names ) ) ? lambdaResult.store_names : [] );
2328
+ const storeIds = rawIds.map( ( s ) => String( s ) );
2329
+ if ( !storeIds.length ) {
2330
+ return [];
2331
+ }
2332
+ const price = await billingDetailsPrice( clientId, billingProductName );
2333
+ const daysInMonth = dayjs().daysInMonth();
2334
+ const stores = await storeService.find(
2335
+ { clientId: clientId, storeId: { $in: storeIds } },
2336
+ { 'storeId': 1, 'storeName': 1, 'storeProfile.country': 1 },
2337
+ );
2338
+ const byId = new Map( stores.map( ( s ) => [ String( s.storeId ), s ] ) );
2339
+ return storeIds.map( ( id ) => {
2340
+ const s = byId.get( String( id ) );
2341
+ return {
2342
+ 'Store Names': s?.storeName || id,
2343
+ 'No of Days to be billed': daysInMonth,
2344
+ 'Final Amount': price,
2345
+ 'Zone': s?.storeProfile?.country || '',
2346
+ };
2347
+ } );
2348
+ }
2349
+
2350
+ // tangoTraffic working days per store, from the latest daily-pricing doc.
2351
+ // Map<storeId, workingdays>; stores with no tangoTraffic record are absent.
2352
+ async function tangoTrafficDaysByStore( clientId ) {
2353
+ const rows = await dailyPriceService.aggregate( [
2354
+ { $match: { clientId: clientId } },
2355
+ { $sort: { dateISO: -1 } },
2356
+ { $limit: 1 },
2357
+ { $unwind: '$stores' },
2358
+ { $unwind: '$stores.products' },
2359
+ { $match: { 'stores.products.productName': 'tangoTraffic' } },
2360
+ { $group: { _id: '$stores.storeId', days: { $max: '$stores.products.workingdays' } } },
2361
+ ] );
2362
+ const map = new Map();
2363
+ for ( const r of rows ) {
2364
+ map.set( String( r._id ), r.days || 0 );
2365
+ }
2366
+ return map;
2367
+ }
2368
+
2369
+ // Per-product detailed Excel export for the Additional Products section.
2370
+ // product selected via req.body.product / req.query.product. Eyetest is the
2371
+ // first; more products plug into the switch as their queries arrive.
2372
+ export async function additionalProductExport( req, res ) {
2373
+ try {
2374
+ const clientId = String( req.body?.clientId ?? req.query?.clientId ?? '' );
2375
+ const product = String( req.body?.product ?? req.query?.product ?? '' ).toLowerCase();
2376
+ // Optional date (YYYY-MM-DD) passed from the UI; defaults to today. Used by
2377
+ // products that report by a specific day (e.g. Remote Optum).
2378
+ const reqDate = String( req.body?.date ?? req.query?.date ?? '' ) || dayjs().format( 'YYYY-MM-DD' );
2379
+ if ( !clientId ) {
2380
+ return res.sendError( 'clientId is required', 400 );
2381
+ }
2382
+
2383
+ if ( product === 'eyetest' ) {
2384
+ // Eye-test streams joined to their store, grouped per store to a stream
2385
+ // count; enriched with store name, country (Zone) and the store's
2386
+ // tangoTraffic working days (No of Days to be billed).
2387
+ const streamRows = await cameraService.aggregate( [
2388
+ { $match: { $and: [
2389
+ { clientId: clientId },
2390
+ { isEyeTestStream: true },
2391
+ { qrCode: { $exists: true } },
2392
+ ] } },
2393
+ { $lookup: {
2394
+ from: 'stores',
2395
+ let: { storeId: '$storeId' },
2396
+ pipeline: [ { $match: { $expr: { $and: [ { $eq: [ '$storeId', '$$storeId' ] } ] } } } ],
2397
+ as: 'stores',
2398
+ } },
2399
+ { $unwind: { path: '$stores', preserveNullAndEmptyArrays: true } },
2400
+ { $group: {
2401
+ _id: '$storeId',
2402
+ streamCount: { $sum: 1 },
2403
+ storeName: { $first: '$stores.storeName' },
2404
+ country: { $first: '$stores.storeProfile.country' },
2405
+ } },
2406
+ { $sort: { storeName: 1 } },
2407
+ ] );
2408
+
2409
+ const daysMap = await tangoTrafficDaysByStore( clientId );
2410
+ const price = ADDITIONAL_PRODUCT_PRICES.eyetest;
2411
+ const exportData = streamRows.map( ( r ) => {
2412
+ const streamCount = r.streamCount || 0;
2413
+ const days = daysMap.get( String( r._id ) ) || 0;
2414
+ // Amount = streams x price (no proration).
2415
+ return {
2416
+ 'storeId': r._id,
2417
+ 'storeName': r.storeName || '',
2418
+ 'Stream Count': streamCount,
2419
+ 'No of Days to be billed': days,
2420
+ 'Amount': streamCount * price,
2421
+ 'Zone': r.country || '',
2422
+ };
2423
+ } );
2424
+
2425
+ if ( !exportData.length ) {
2426
+ return res.sendError( 'No data', 204 );
2427
+ }
2428
+ await download( exportData, res );
2429
+ return;
2430
+ }
2431
+
2432
+ if ( product === 'planogram' ) {
2433
+ // Unique storeName from the planograms collection, enriched with the
2434
+ // store's country (Zone) and tangoTraffic working days. Amount is the
2435
+ // flat planogram price per store.
2436
+ const planoRows = await planogramService.aggregate( [
2437
+ { $match: { clientId: clientId } },
2438
+ { $group: { _id: '$storeName', storeId: { $first: '$storeId' } } },
2439
+ { $lookup: {
2440
+ from: 'stores',
2441
+ let: { sid: '$storeId' },
2442
+ pipeline: [ { $match: { $expr: { $eq: [ '$storeId', '$$sid' ] } } } ],
2443
+ as: 'store',
2444
+ } },
2445
+ { $unwind: { path: '$store', preserveNullAndEmptyArrays: true } },
2446
+ { $project: { _id: 0, storeName: '$_id', storeId: 1, country: '$store.storeProfile.country' } },
2447
+ { $sort: { storeName: 1 } },
2448
+ ] );
2449
+
2450
+ const price = ADDITIONAL_PRODUCT_PRICES.planogram;
2451
+ // Planogram bills the full current month — every store shows the number of
2452
+ // days in the current calendar month (not per-store working days).
2453
+ const daysInMonth = dayjs().daysInMonth();
2454
+ const exportData = planoRows.map( ( r ) => {
2455
+ // Amount = flat planogram price per store (no proration).
2456
+ return {
2457
+ 'storeName': r.storeName || '',
2458
+ 'No of Days to be billed': daysInMonth,
2459
+ 'Amount': price,
2460
+ 'Zone': r.country || '',
2461
+ };
2462
+ } );
2463
+
2464
+ if ( !exportData.length ) {
2465
+ return res.sendError( 'No data', 204 );
2466
+ }
2467
+ await download( exportData, res );
2468
+ return;
2469
+ }
2470
+
2471
+ if ( product === 'aimanager' ) {
2472
+ // AI Manager bills the same stores as tangoTraffic running > 1 day. One
2473
+ // row per such store with its tangoTraffic working days (No of Days to be
2474
+ // billed), the flat AI Manager price, and the store's country (Zone).
2475
+ const aiRows = await dailyPriceService.aggregate( [
2476
+ { $match: { clientId: clientId } },
2477
+ { $sort: { dateISO: -1 } },
2478
+ { $limit: 1 },
2479
+ { $unwind: '$stores' },
2480
+ { $unwind: '$stores.products' },
2481
+ { $match: { 'stores.products.productName': 'tangoTraffic', 'stores.products.workingdays': { $gt: 1 } } },
2482
+ { $group: {
2483
+ _id: '$stores.storeId',
2484
+ storeName: { $first: '$stores.storeName' },
2485
+ days: { $max: '$stores.products.workingdays' },
2486
+ } },
2487
+ { $lookup: {
2488
+ from: 'stores',
2489
+ let: { sid: '$_id' },
2490
+ pipeline: [ { $match: { $expr: { $eq: [ '$storeId', '$$sid' ] } } } ],
2491
+ as: 'store',
2492
+ } },
2493
+ { $unwind: { path: '$store', preserveNullAndEmptyArrays: true } },
2494
+ { $project: { _id: 0, storeId: '$_id', storeName: 1, days: 1, country: '$store.storeProfile.country' } },
2495
+ { $sort: { storeName: 1 } },
2496
+ ] );
2497
+
2498
+ const price = ADDITIONAL_PRODUCT_PRICES.aiManager;
2499
+ const exportData = aiRows.map( ( r ) => {
2500
+ // Amount = flat AI Manager price per store (no proration).
2501
+ return {
2502
+ 'Store Name': r.storeName || '',
2503
+ 'No of Days to be billed': r.days || 0,
2504
+ 'Final Amount': price,
2505
+ 'Zone': r.country || '',
2506
+ };
2507
+ } );
2508
+
2509
+ if ( !exportData.length ) {
2510
+ return res.sendError( 'No data', 204 );
2511
+ }
2512
+ await download( exportData, res );
2513
+ return;
2514
+ }
2515
+
2516
+ if ( product === 'vms' ) {
2517
+ const exportData = await lambdaStoreExport(
2518
+ clientId,
2519
+ 'https://ppf3l3mxc2lorh5hkrsj6zwyim0bupxw.lambda-url.ap-south-1.on.aws/',
2520
+ 'VMS',
2521
+ );
2522
+ if ( !exportData.length ) {
2523
+ return res.sendError( 'No data', 204 );
2524
+ }
2525
+ await download( exportData, res );
2526
+ return;
2527
+ }
2528
+
2529
+ if ( product === 'runai' ) {
2530
+ const exportData = await lambdaStoreExport(
2531
+ clientId,
2532
+ 'https://mdm3mf7wuficgv3jjspkws2nu40azlsc.lambda-url.ap-south-1.on.aws/',
2533
+ 'Run AI',
2534
+ );
2535
+ console.log( '🚀 ~ additionalProductExport ~ exportData:', exportData );
2536
+ if ( !exportData.length ) {
2537
+ return res.sendError( 'No data', 204 );
2538
+ }
2539
+ await download( exportData, res );
2540
+ return;
2541
+ }
2542
+
2543
+ if ( product === 'remoteoptum' ) {
2544
+ // Detailed optom-audit export from the remote_optom_steps_summary index
2545
+ // for the given date (passed from the UI, defaults to today). Each record
2546
+ // is one row; the nested `steps` object is flattened into TRUE/FALSE
2547
+ // columns in a fixed (alphabetical) order.
2548
+ const STEP_COLUMNS = [
2549
+ 'Adjust-Phoropter', 'DuoChrome-Test', 'Explanation', 'Final-Prescription',
2550
+ 'Handover', 'History-Taking', 'JCC', 'Near-Vision', 'Personal-Intro',
2551
+ 'Subjective-Refraction', 'Trial-Frame', 'VA-Check',
2552
+ ];
2553
+ let optomHits = [];
2554
+ try {
2555
+ const osRes = await getOpenSearchData( 'remote_optom_steps_summary', {
2556
+ // A single day is well under the 10k window; pull up to the cap.
2557
+ // NOTE: do NOT sort on StartTime — it's an analyzed text field, and
2558
+ // sorting on it makes OpenSearch return ZERO hits. The day filter is
2559
+ // enough; rows come back in index order.
2560
+ size: 10000,
2561
+ query: { term: { 'Date': reqDate } },
2562
+ } );
2563
+ optomHits = osRes?.body?.hits?.hits || osRes?.hits?.hits || [];
2564
+ } catch ( osErr ) {
2565
+ logger.error( { error: osErr, function: 'additionalProductExport.remoteOptum', clientId } );
2566
+ return res.sendError( 'Failed to fetch Remote Optum data', 502 );
2567
+ }
2568
+
2569
+ const exportData = optomHits.map( ( h ) => {
2570
+ const s = h._source || {};
2571
+ const steps = s.steps || {};
2572
+ const row = {
2573
+ 'storeName': s.storeName || '',
2574
+ 'engagementId': s.engagementId || '',
2575
+ 'queue_id': s.queue_id || '',
2576
+ 'optm_name': s.optm_name || '',
2577
+ 'optm_id': s.optm_id || '',
2578
+ 'optm_Emailid': s.optm_Emailid || '',
2579
+ 'Date': s.Date || '',
2580
+ 'StartTime': s.StartTime || '',
2581
+ 'EndTime': s.EndTime || '',
2582
+ 'Duration (min)': s['Duration (min)'] != null ? s['Duration (min)'] : '',
2583
+ };
2584
+ for ( const col of STEP_COLUMNS ) {
2585
+ row[col] = steps[col] === true ? 'TRUE' : 'FALSE';
2586
+ }
2587
+ return row;
2588
+ } );
2589
+
2590
+ if ( !exportData.length ) {
2591
+ return res.sendError( 'No data', 204 );
2592
+ }
2593
+ await download( exportData, res );
2594
+ return;
2595
+ }
2596
+
2597
+ return res.sendError( `Unsupported product: ${product}`, 400 );
2598
+ } catch ( error ) {
2599
+ logger.error( { error: error, function: 'additionalProductExport' } );
2600
+ return res.sendError( error, 500 );
2601
+ }
2602
+ }
@@ -19,6 +19,7 @@ import { findOneApplicationDefault } from '../services/applicationDefault.servic
19
19
  import * as assignedStoreService from '../services/assignedStore.service.js';
20
20
  import * as bankTransactionService from '../services/bankTransaction.service.js';
21
21
  import { getUsdInrRate, getAdditionalProducts } from './brandsBilling.controller.js';
22
+ import { applyInvoiceToPurchaseOrder } from './purchaseOrder.controller.js';
22
23
 
23
24
  // Pulls CSM + Finance head emails (stored under applicationDefault
24
25
  // type=invoice, subType=heads) AND the per-client CSMs assigned via
@@ -126,8 +127,11 @@ export async function createInvoice( req, res ) {
126
127
  return res.sendError( 'clientId is required for customInvoice', 400 );
127
128
  }
128
129
  const Finacialyear = getCurrentFinancialYear();
130
+ // Quarterly/half-yearly/yearly advance invoices use the separate TINV-
131
+ // series with its own per-FY counter; monthly advance + normal stay INV-.
132
+ const invPrefix = invoicePrefixFor( req.body.advanceInvoice, req.body.advancePeriod );
129
133
  const previousinvoice = await invoiceService.findandsort(
130
- { invoice: { $regex: `^INV-${Finacialyear}-` } },
134
+ { invoice: { $regex: `^${invPrefix}${Finacialyear}-` } },
131
135
  {},
132
136
  { invoiceIndex: -1 },
133
137
  );
@@ -167,7 +171,7 @@ export async function createInvoice( req, res ) {
167
171
  Math.round( Number( req.body.totalAmount ) || 0 );
168
172
 
169
173
  const data = {
170
- invoice: `INV-${Finacialyear}-${invoiceNo}`,
174
+ invoice: `${invPrefix}${Finacialyear}-${invoiceNo}`,
171
175
  invoiceIndex: invoiceNo,
172
176
  clientId: req.body.clientId,
173
177
  groupId: req.body.groupId || undefined,
@@ -192,8 +196,24 @@ export async function createInvoice( req, res ) {
192
196
  advanceInvoice: req.body.advanceInvoice || false,
193
197
  advancePeriod: req.body.advanceInvoice ? ( req.body.advancePeriod || 'monthly' ) : undefined,
194
198
  advanceMonths: customAdvanceMonths,
199
+ // Purchase order this invoice is mapped to (optional).
200
+ purchaseOrderNumber: req.body.purchaseOrderNumber || undefined,
195
201
  };
196
202
  const created = await invoiceService.create( data );
203
+ // Map to a PO (deduct its remaining balance + log) when one was picked.
204
+ if ( data.purchaseOrderNumber ) {
205
+ try {
206
+ await applyInvoiceToPurchaseOrder( {
207
+ clientId: data.clientId,
208
+ purchaseOrderNumber: data.purchaseOrderNumber,
209
+ invoice: data.invoice,
210
+ amount: data.totalAmount,
211
+ req,
212
+ } );
213
+ } catch ( poErr ) {
214
+ logger.error( { error: poErr, function: 'createInvoice.applyPO', invoice: data.invoice } );
215
+ }
216
+ }
197
217
  const logObj = {
198
218
  userName: req.user?.userName,
199
219
  email: req.user?.email,
@@ -216,11 +236,41 @@ export async function createInvoice( req, res ) {
216
236
 
217
237
  for ( let group of invoiceGroupList ) {
218
238
  let Finacialyear = getCurrentFinancialYear();
219
- // Scope the highest-index lookup to invoices created in the current FY
220
- // (invoice IDs are `INV-${FY}-${index}`). Without this scope the new FY
221
- // would continue the previous year's sequence instead of resetting.
239
+
240
+ // If an ADVANCE invoice already covers the CURRENT month for this group
241
+ // (i.e. it has a product line item labelled with the current MMM YYYY),
242
+ // we must NOT generate another advance invoice for it. Instead generate a
243
+ // normal monthly invoice using the store counts from that advance invoice.
244
+ const currentMonthLabel = dayjs().format( 'MMM YYYY' );
245
+ let advanceCoverProducts = null;
246
+ if ( group.advanceInvoice ) {
247
+ const existingAdvance = await invoiceService.findOne( {
248
+ 'groupId': group._id,
249
+ 'advanceInvoice': true,
250
+ 'products.month': currentMonthLabel,
251
+ } );
252
+ if ( existingAdvance ) {
253
+ // Take this month's line items from the advance invoice (they carry the
254
+ // store counts + prices the advance covered) and bill them as a normal
255
+ // monthly invoice.
256
+ advanceCoverProducts = ( existingAdvance.products || [] )
257
+ .filter( ( p ) => p.month === currentMonthLabel )
258
+ .map( ( p ) => ( { ...( p._doc || p ), month: currentMonthLabel } ) );
259
+ }
260
+ }
261
+ // Effective advance flag: forced OFF when an advance invoice already covers
262
+ // this month, so the rest of the loop runs the normal monthly path.
263
+ const isAdvance = group.advanceInvoice && !advanceCoverProducts;
264
+
265
+ // Quarterly/half-yearly/yearly advance invoices use the separate TINV-
266
+ // series with its own per-FY counter; monthly advance + normal stay INV-.
267
+ const invPrefix = invoicePrefixFor( isAdvance, group.advancePeriod );
268
+ // Scope the highest-index lookup to invoices created in the current FY for
269
+ // THIS series. Without the FY scope the new FY would continue the previous
270
+ // year's sequence; without the prefix scope the two series would share one
271
+ // counter.
222
272
  let previousinvoice = await invoiceService.findandsort(
223
- { invoice: { $regex: `^INV-${Finacialyear}-` } },
273
+ { invoice: { $regex: `^${invPrefix}${Finacialyear}-` } },
224
274
  {},
225
275
  { invoiceIndex: -1 },
226
276
  );
@@ -232,10 +282,16 @@ export async function createInvoice( req, res ) {
232
282
 
233
283
  let address = group.addressLineOne + group.addressLineTwo + group.city + ',' + group.state + ',' + group.country + ' -' + group.pinCode;
234
284
  let getClient = await clientService.findOne( { clientId: group.clientId, status: 'active' } );
235
- let baseDate = group.advanceInvoice ? dayjs().add( 1, 'month' ).startOf( 'month' ) : dayjs();
285
+ // Advance bills next month; normal (incl. the advance-already-covered case)
286
+ // bills the current month.
287
+ let baseDate = isAdvance ? dayjs().add( 1, 'month' ).startOf( 'month' ) : dayjs();
236
288
  let products;
237
289
 
238
- if ( getClient?.priceType === 'standard' ) {
290
+ if ( advanceCoverProducts ) {
291
+ // Reuse the advance invoice's current-month line items (store counts +
292
+ // prices already set) and bill them as a normal monthly invoice.
293
+ products = advanceCoverProducts;
294
+ } else if ( getClient?.priceType === 'standard' ) {
239
295
  products = await standardPrice( group, getClient, baseDate );
240
296
  } else {
241
297
  products = await stepPrice( group, getClient );
@@ -247,18 +303,22 @@ export async function createInvoice( req, res ) {
247
303
  // store-based product lines. Added BEFORE the multi-month expansion below
248
304
  // so they repeat per month and are included in the taxable subtotal, just
249
305
  // like the standard products. Returns [] for any client without extras.
250
- const extraProducts = await getAdditionalProducts( group.clientId );
251
- for ( const ep of extraProducts ) {
252
- products.push( {
253
- productName: ep.productName,
254
- period: 'fullmonth',
255
- storeCount: ep.quantity,
256
- amount: ep.total,
257
- price: ep.price,
258
- description: ep.productName,
259
- HsnNumber: '998314',
260
- month: baseDate.format( 'MMM YYYY' ),
261
- } );
306
+ // Skip when reusing advance-invoice line items — those already include any
307
+ // additional products from when the advance invoice was generated.
308
+ if ( !advanceCoverProducts ) {
309
+ const extraProducts = await getAdditionalProducts( group.clientId );
310
+ for ( const ep of extraProducts ) {
311
+ products.push( {
312
+ productName: ep.productName,
313
+ period: 'fullmonth',
314
+ storeCount: ep.quantity,
315
+ amount: ep.total,
316
+ price: ep.price,
317
+ description: ep.productName,
318
+ HsnNumber: '998314',
319
+ month: baseDate.format( 'MMM YYYY' ),
320
+ } );
321
+ }
262
322
  }
263
323
 
264
324
  // Billing horizon in months. Advance and normal cycle are independent —
@@ -269,13 +329,15 @@ export async function createInvoice( req, res ) {
269
329
  // product is repeated once per month, labelled with that month, at the
270
330
  // normal monthly amount. One invoice then bills the whole period.
271
331
  // (advancePeriod uses 'halfyearly'; paymentCycle uses 'quarter'/'halfYearly'
272
- // — map both spellings.)
332
+ // — map both spellings.) Uses isAdvance (not group.advanceInvoice) so the
333
+ // advance-already-covered case bills a single normal month.
273
334
  const advancePeriodMonths = { quarterly: 3, halfyearly: 6, yearly: 12 };
274
335
  const paymentCycleMonths = { quarter: 3, quarterly: 3, halfYearly: 6, halfyearly: 6, yearly: 12 };
275
- const advanceMonths = group.advanceInvoice ?
336
+ const advanceMonths = isAdvance ?
276
337
  ( advancePeriodMonths[group.advancePeriod] || 1 ) :
277
338
  ( paymentCycleMonths[group.paymentCycle] || 1 );
278
- if ( advanceMonths > 1 ) {
339
+ // No re-expansion when reusing advance line items (already this month only).
340
+ if ( !advanceCoverProducts && advanceMonths > 1 ) {
279
341
  const expanded = [];
280
342
  for ( let m = 0; m < advanceMonths; m++ ) {
281
343
  const monthLabel = baseDate.add( m, 'month' ).format( 'MMM YYYY' );
@@ -292,8 +354,11 @@ export async function createInvoice( req, res ) {
292
354
  // multi-month expansion so it's billed ONCE per invoice (not per month).
293
355
  // It's a normal line item, so it lands in the taxable subtotal below.
294
356
  try {
357
+ // Use the group's basepricing doc when group-wise pricing applies,
358
+ // else the brand-level doc.
359
+ const { query: oneTimeQuery } = await resolveBasePricingScope( group, getClient );
295
360
  const oneTimeBp = await basepricingService.findOne(
296
- { clientId: group.clientId },
361
+ oneTimeQuery,
297
362
  { oneTimeFeePerStore: 1 },
298
363
  );
299
364
  const oneTimeFeePerStore = Number( oneTimeBp?.oneTimeFeePerStore ) || 0;
@@ -412,6 +477,36 @@ export async function createInvoice( req, res ) {
412
477
 
413
478
  ] );
414
479
 
480
+ // For TINV (advance quarterly/half-yearly/yearly) invoices, capture the
481
+ // actual store list — storeId, storeName and each store's products with
482
+ // working days — onto the invoice from the latest daily-pricing doc.
483
+ let storeDetails = [];
484
+ if ( invPrefix === 'TINV-' ) {
485
+ const dpDoc = await dailyPricingService.aggregate( [
486
+ { $match: { clientId: group.clientId } },
487
+ { $sort: { dateISO: -1 } },
488
+ { $limit: 1 },
489
+ { $project: { stores: {
490
+ $filter: { input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', group.stores ] } },
491
+ } } },
492
+ ] );
493
+ const dpStores = dpDoc?.[0]?.stores || [];
494
+ storeDetails = dpStores
495
+ .map( ( s ) => ( {
496
+ storeId: s.storeId,
497
+ storeName: s.storeName,
498
+ // Only products that actually ran this month (workingdays > 0).
499
+ products: ( s.products || [] )
500
+ .filter( ( p ) => ( Number( p.workingdays ) || 0 ) > 0 )
501
+ .map( ( p ) => ( {
502
+ productName: p.productName,
503
+ workingdays: Number( p.workingdays ) || 0,
504
+ } ) ),
505
+ } ) )
506
+ // Drop stores left with no running products.
507
+ .filter( ( s ) => s.products.length > 0 );
508
+ }
509
+
415
510
  // billingDate is the actual invoice (creation) date — even for advance
416
511
  // invoices. The advance MONTH is reflected only in the product lines /
417
512
  // monthOfbilling (driven by baseDate), not in the billing date itself.
@@ -425,7 +520,7 @@ export async function createInvoice( req, res ) {
425
520
  let data = {
426
521
  groupName: group.groupName,
427
522
  groupId: group._id,
428
- invoice: req.body.invoiceId ? req.body.invoiceId : `INV-${Finacialyear}-${invoiceNo}`,
523
+ invoice: req.body.invoiceId ? req.body.invoiceId : `${invPrefix}${Finacialyear}-${invoiceNo}`,
429
524
  products: products,
430
525
  status: 'pendingCsm',
431
526
  amount: Math.round( amount ),
@@ -439,12 +534,19 @@ export async function createInvoice( req, res ) {
439
534
  clientId: group.clientId,
440
535
  paymentMethod: 'Online',
441
536
  billingDate: new Date( invoicedate ),
442
- stores: totalStoreCount.length ? totalStoreCount[0].stores.length : 0,
537
+ // For the advance-already-covered case, take the store count from the
538
+ // advance invoice's line items; otherwise the live running-store count.
539
+ stores: advanceCoverProducts ?
540
+ Math.max( 0, ...advanceCoverProducts.map( ( p ) => Number( p.storeCount ) || 0 ) ) :
541
+ ( totalStoreCount.length ? totalStoreCount[0].stores.length : 0 ),
542
+ // TINV invoices carry the full store list (id, name, product workingdays).
543
+ storeDetails: storeDetails,
443
544
  currency: group.currency ? group.currency : 'inr',
444
545
  monthOfbilling: baseDate.format( 'MM' ),
445
546
  dueDate: dueDate,
446
- advanceInvoice: group.advanceInvoice || false,
447
- advancePeriod: group.advanceInvoice ? ( group.advancePeriod || 'monthly' ) : undefined,
547
+ // Stored as a NORMAL invoice when an advance already covered this month.
548
+ advanceInvoice: isAdvance || false,
549
+ advancePeriod: isAdvance ? ( group.advancePeriod || 'monthly' ) : undefined,
448
550
  advanceMonths: advanceMonths,
449
551
  };
450
552
 
@@ -473,12 +575,12 @@ export async function createInvoice( req, res ) {
473
575
  await invoiceService.create( data );
474
576
 
475
577
  if ( !req.body.regenrate ) {
476
- let invoiceType = group.advanceInvoice ? 'advance' : '';
578
+ let invoiceType = isAdvance ? 'advance' : '';
477
579
  let logObj = {
478
580
  userName: req.user?.userName,
479
581
  email: req.user?.email,
480
582
  clientId: group.clientId,
481
- logSubType: group.advanceInvoice ? 'advanceInvoiceCreated' : 'invoiceCreated',
583
+ logSubType: isAdvance ? 'advanceInvoiceCreated' : 'invoiceCreated',
482
584
  logType: 'invoice',
483
585
  date: new Date(),
484
586
  changes: [ `${data.invoice} ${invoiceType} invoice has been generated for ${group.groupName} for ${baseDate.format( 'MMM YYYY' )}` ],
@@ -508,6 +610,17 @@ function getCurrentFinancialYear() {
508
610
  }
509
611
  }
510
612
 
613
+ // Invoice-number prefix. Advance invoices for quarterly / half-yearly / yearly
614
+ // use a separate "TINV-" series with its own counter; everything else
615
+ // (including MONTHLY advance) uses the normal "INV-" series.
616
+ function invoicePrefixFor( advanceInvoice, advancePeriod ) {
617
+ const multiMonth = [ 'quarterly', 'halfyearly', 'yearly' ];
618
+ if ( advanceInvoice && multiMonth.includes( String( advancePeriod || '' ).toLowerCase() ) ) {
619
+ return 'TINV-';
620
+ }
621
+ return 'INV-';
622
+ }
623
+
511
624
 
512
625
  // ---------------------------------------------------------------------------
513
626
  // Shared annexure builder. Anchored to the invoice's BILLING month and
@@ -526,7 +639,7 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
526
639
  // converting here would make the annexure unit price disagree with the actual
527
640
  // billed amount (e.g. a $45 negotiated price wrongly shown as $0.48).
528
641
 
529
- const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1, 'priceType': 1 } );
642
+ const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1, 'priceType': 1, 'billingGroupWisePricing': 1 } );
530
643
  const billingTypeMap = {};
531
644
  ( annexClient?.planDetails?.product || [] ).forEach( ( p ) => {
532
645
  billingTypeMap[p.productName] = p.billingType || 'perStore';
@@ -538,7 +651,18 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
538
651
  // appear once per tier (e.g. the same store listed at $45 and again at $40).
539
652
  // Instead we pull the tiers here and assign each store a single tier price
540
653
  // below, in JS, by its 1-based index within the product.
541
- const pricingDoc = await basepricingService.findOne( { clientId: invoiceInfo.clientId }, { standard: 1, step: 1 } );
654
+ // Group-wise pricing: read the billing group's own doc when enabled and one
655
+ // exists; otherwise the brand-level doc.
656
+ let pricingDocQuery = { clientId: invoiceInfo.clientId, groupId: { $exists: false } };
657
+ if ( annexClient?.billingGroupWisePricing && getgroup?._id ) {
658
+ const grpDoc = await basepricingService.findOne(
659
+ { clientId: invoiceInfo.clientId, groupId: String( getgroup._id ) }, { _id: 1 },
660
+ );
661
+ if ( grpDoc ) {
662
+ pricingDocQuery = { clientId: invoiceInfo.clientId, groupId: String( getgroup._id ) };
663
+ }
664
+ }
665
+ const pricingDoc = await basepricingService.findOne( pricingDocQuery, { standard: 1, step: 1 } );
542
666
  const isStep = annexClient?.priceType === 'step';
543
667
  const tiersByProduct = {};
544
668
  ( ( isStep ? pricingDoc?.step : pricingDoc?.standard ) || [] ).forEach( ( p ) => {
@@ -693,7 +817,7 @@ export async function invoiceDownload( req, res ) {
693
817
  companyAddress: invoiceInfo.companyAddress,
694
818
  PlaceOfSupply: invoiceInfo.PlaceOfSupply,
695
819
  GSTNumber: invoiceInfo.GSTNumber,
696
- PoNum: getgroup?.po,
820
+ PoNum: invoiceInfo.purchaseOrderNumber || getgroup?.po || '',
697
821
  amountwords: AmountinWords,
698
822
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
699
823
  currencyType: invoiceCurrency,
@@ -889,7 +1013,7 @@ async function buildInvoicePdfBuffer( invoiceId ) {
889
1013
  companyAddress: invoiceInfo.companyAddress,
890
1014
  PlaceOfSupply: invoiceInfo.PlaceOfSupply,
891
1015
  GSTNumber: invoiceInfo.GSTNumber,
892
- PoNum: getgroup?.po,
1016
+ PoNum: invoiceInfo.purchaseOrderNumber || getgroup?.po || '',
893
1017
  amountwords: AmountinWords,
894
1018
  Terms: `Term ${getgroup?.paymentTerm ? getgroup?.paymentTerm : '30'}`,
895
1019
  currencyType: invoiceCurrency,
@@ -1080,6 +1204,25 @@ function inWords( num ) {
1080
1204
  }
1081
1205
 
1082
1206
 
1207
+ // Resolve which basepricing doc applies to a billing group. When the client
1208
+ // has billingGroupWisePricing enabled AND a doc exists for this group, returns
1209
+ // that group's doc; otherwise falls back to the brand-level doc (groupId unset).
1210
+ // Returns { query, groupId } where query is the mongo filter for the chosen doc
1211
+ // and groupId is the group id to use in aggregation lookups (or null for brand).
1212
+ async function resolveBasePricingScope( group, getClient ) {
1213
+ const brandQuery = { clientId: group.clientId, groupId: { $exists: false } };
1214
+ if ( getClient?.billingGroupWisePricing && group?._id ) {
1215
+ const groupIdStr = String( group._id );
1216
+ const groupDoc = await basepricingService.findOne(
1217
+ { clientId: group.clientId, groupId: groupIdStr }, { _id: 1 },
1218
+ );
1219
+ if ( groupDoc ) {
1220
+ return { query: { clientId: group.clientId, groupId: groupIdStr }, groupId: groupIdStr };
1221
+ }
1222
+ }
1223
+ return { query: brandQuery, groupId: null };
1224
+ }
1225
+
1083
1226
  async function standardPrice( group, getClient, baseDate ) {
1084
1227
  console.log( '🚀 ~ standardPrice ~ baseDate:', baseDate.format( 'MMM YYYY' ) );
1085
1228
  const currentMonthDays = dayjs().daysInMonth();
@@ -1088,6 +1231,9 @@ async function standardPrice( group, getClient, baseDate ) {
1088
1231
  // Computed once so the aggregation pipelines can inline a $literal.
1089
1232
  const isFlatPricing = group.proRata === 'flat';
1090
1233
  console.log( '🚀 ~ standardPrice ~ isFlatPricing:', isFlatPricing );
1234
+ // Which basepricing doc applies (group-wise vs brand-level). pricingGroupId is
1235
+ // used in the $lookup pipeline so the join picks the right doc.
1236
+ const { groupId: pricingGroupId } = await resolveBasePricingScope( group, getClient );
1091
1237
  let billingTypeMap = {};
1092
1238
  if ( getClient?.planDetails?.product ) {
1093
1239
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1224,7 +1370,14 @@ async function standardPrice( group, getClient, baseDate ) {
1224
1370
  {
1225
1371
  $match: {
1226
1372
  $expr: {
1227
- $eq: [ '$clientId', '$$clientId' ],
1373
+ $and: [
1374
+ { $eq: [ '$clientId', '$$clientId' ] },
1375
+ // Match the resolved doc: a specific group's doc when group-wise
1376
+ // pricing applies, else the brand-level doc (no groupId).
1377
+ pricingGroupId ?
1378
+ { $eq: [ '$groupId', pricingGroupId ] } :
1379
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1380
+ ],
1228
1381
  },
1229
1382
  },
1230
1383
  },
@@ -1419,7 +1572,12 @@ async function standardPrice( group, getClient, baseDate ) {
1419
1572
  from: 'basepricings',
1420
1573
  let: { clientId: group.clientId },
1421
1574
  pipeline: [
1422
- { $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
1575
+ { $match: { $expr: { $and: [
1576
+ { $eq: [ '$clientId', '$$clientId' ] },
1577
+ pricingGroupId ?
1578
+ { $eq: [ '$groupId', pricingGroupId ] } :
1579
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1580
+ ] } } },
1423
1581
  { $project: { standard: 1 } },
1424
1582
  ],
1425
1583
  as: 'basepricing',
@@ -1527,6 +1685,8 @@ async function stepPrice( group, getClient ) {
1527
1685
  // 'flat' => every store billed for full month.
1528
1686
  // 'prorate' => actual working days. See standardPrice for the same flag.
1529
1687
  const isFlatPricing = group.proRata === 'flat';
1688
+ // Which basepricing doc applies (group-wise vs brand-level).
1689
+ const { query: pricingDocQuery, groupId: pricingGroupId } = await resolveBasePricingScope( group, getClient );
1530
1690
  let billingTypeMap = {};
1531
1691
  if ( getClient?.planDetails?.product ) {
1532
1692
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1628,7 +1788,12 @@ async function stepPrice( group, getClient ) {
1628
1788
  from: 'basepricings',
1629
1789
  let: { clientId: group.clientId },
1630
1790
  pipeline: [
1631
- { $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
1791
+ { $match: { $expr: { $and: [
1792
+ { $eq: [ '$clientId', '$$clientId' ] },
1793
+ pricingGroupId ?
1794
+ { $eq: [ '$groupId', pricingGroupId ] } :
1795
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1796
+ ] } } },
1632
1797
  { $project: { step: 1 } },
1633
1798
  ],
1634
1799
  as: 'basepricing',
@@ -1637,7 +1802,7 @@ async function stepPrice( group, getClient ) {
1637
1802
  { $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
1638
1803
  ] );
1639
1804
 
1640
- let stepPriceData = await basepricingService.findOne( { clientId: group.clientId } );
1805
+ let stepPriceData = await basepricingService.findOne( pricingDocQuery );
1641
1806
  let pricingRanges = stepPriceData?.step || [];
1642
1807
  let defaultPrice = pricingRanges.length > 0 ? pricingRanges[0].negotiatePrice : 0;
1643
1808
 
@@ -1682,7 +1847,7 @@ async function stepPrice( group, getClient ) {
1682
1847
  // Drop eachStore products — those are handled by the per-store branch above.
1683
1848
  perStoreRows = perStoreRows.filter( ( p ) => !eachStoreProductNames.includes( p.productName ) );
1684
1849
 
1685
- const stepPriceRecord = await basepricingService.findOne( { clientId: group.clientId } );
1850
+ const stepPriceRecord = await basepricingService.findOne( pricingDocQuery );
1686
1851
  // Tiers ordered by range start so per-store tier assignment walks low-to-high.
1687
1852
  const pricing = ( stepPriceRecord?.step || [] ).slice().sort( ( a, b ) => {
1688
1853
  const aStart = parseInt( String( a.storeRange || '0' ).split( '-' )[0], 10 ) || 0;
@@ -2466,7 +2631,14 @@ export async function checkPaymentStatus( req, res ) {
2466
2631
 
2467
2632
  export async function getInvoice( req, res ) {
2468
2633
  try {
2469
- let invoice = await invoiceService.findOne( { _id: req.params.invoiceId } );
2634
+ // Accept either a Mongo _id or an invoice NUMBER. A 24-char hex string is
2635
+ // treated as an _id; anything else (e.g. "INV-2026-00012") matches the
2636
+ // invoice number — so the Purchase Order popup can preview by number.
2637
+ const idParam = String( req.params.invoiceId || '' );
2638
+ const isObjectId = /^[a-fA-F0-9]{24}$/.test( idParam );
2639
+ let invoice = await invoiceService.findOne(
2640
+ isObjectId ? { _id: idParam } : { invoice: idParam },
2641
+ );
2470
2642
  if ( !invoice ) {
2471
2643
  return res.sendError( 'Invoice not found', 404 );
2472
2644
  }
@@ -2496,7 +2668,7 @@ export async function updateInvoice( req, res ) {
2496
2668
  'companyName', 'companyAddress', 'GSTNumber', 'PlaceOfSupply',
2497
2669
  'groupName', 'groupId', 'stores', 'billingDate', 'dueDate',
2498
2670
  'currency', 'status', 'paymentStatus', 'products', 'tax',
2499
- 'amount', 'totalAmount', 'paymentMethod',
2671
+ 'amount', 'totalAmount', 'paymentMethod', 'purchaseOrderNumber',
2500
2672
  ];
2501
2673
 
2502
2674
  allowedFields.forEach( ( field ) => {
@@ -2522,6 +2694,23 @@ export async function updateInvoice( req, res ) {
2522
2694
 
2523
2695
  let updatedInvoice = await invoiceService.updateOne( { _id: req.body._id }, updateData );
2524
2696
 
2697
+ // PO mapping on edit: when a (new) PO number is set, deduct the invoice
2698
+ // amount from that PO. applyInvoiceToPurchaseOrder is idempotent per
2699
+ // invoice+PO, so re-saving the same PO won't double-deduct.
2700
+ if ( updateData.purchaseOrderNumber && updateData.purchaseOrderNumber !== invoice.purchaseOrderNumber ) {
2701
+ try {
2702
+ await applyInvoiceToPurchaseOrder( {
2703
+ clientId: invoice.clientId,
2704
+ purchaseOrderNumber: updateData.purchaseOrderNumber,
2705
+ invoice: invoice.invoice,
2706
+ amount: updateData.totalAmount != null ? updateData.totalAmount : invoice.totalAmount,
2707
+ req,
2708
+ } );
2709
+ } catch ( poErr ) {
2710
+ logger.error( { error: poErr, function: 'updateInvoice.applyPO', invoice: invoice.invoice } );
2711
+ }
2712
+ }
2713
+
2525
2714
  let logObj = {
2526
2715
  userName: req.user?.userName,
2527
2716
  email: req.user?.email,
@@ -85,6 +85,7 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
85
85
  billingDetails: 1,
86
86
  price: 1,
87
87
  priceType: 1,
88
+ billingGroupWisePricing: 1,
88
89
  virtualAccount: 1,
89
90
  paymentInvoice: 1,
90
91
  },
@@ -259,6 +260,7 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
259
260
  currentPlanInfo.dueLimitReached = getPI;
260
261
  currentPlanInfo.price = clientInfo[0].price || '--';
261
262
  currentPlanInfo.priceType = clientInfo[0].priceType || '--';
263
+ currentPlanInfo.billingGroupWisePricing = clientInfo[0].billingGroupWisePricing || false;
262
264
  currentPlanInfo.subscriptionType = clientInfo[0].planDetails.subscriptionType || '--';
263
265
  currentPlanInfo.subscriptionPeriod = clientInfo[0].planDetails.subscriptionPeriod || '--';
264
266
  currentPlanInfo.storeCount = storeCount || '--';
@@ -2246,7 +2248,15 @@ export const invoiceList = async ( req, res ) => {
2246
2248
 
2247
2249
  export const priceList = async ( req, res ) => {
2248
2250
  try {
2249
- let pricingDetails = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1, oneTimeFeePerStore: 1 } );
2251
+ // Group-wise pricing: when a groupId is sent, read that group's own
2252
+ // basepricing doc; otherwise read the brand-level doc (groupId unset).
2253
+ const priceQuery = { clientId: req.body.clientId };
2254
+ if ( req.body.groupId ) {
2255
+ priceQuery.groupId = req.body.groupId;
2256
+ } else {
2257
+ priceQuery.groupId = { $exists: false };
2258
+ }
2259
+ let pricingDetails = await basePricingService.findOne( priceQuery, { standard: 1, step: 1, oneTimeFeePerStore: 1 } );
2250
2260
  if ( !pricingDetails ) {
2251
2261
  return res.sendError( 'no data found', 204 );
2252
2262
  }
@@ -2360,7 +2370,15 @@ export const priceList = async ( req, res ) => {
2360
2370
 
2361
2371
  export const pricingListUpdate = async ( req, res ) => {
2362
2372
  try {
2363
- let getPriceInfo = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1 } );
2373
+ // Group-wise pricing: target the group's own basepricing doc when a groupId
2374
+ // is sent; otherwise the brand-level doc (groupId unset).
2375
+ const priceQuery = { clientId: req.body.clientId };
2376
+ if ( req.body.groupId ) {
2377
+ priceQuery.groupId = req.body.groupId;
2378
+ } else {
2379
+ priceQuery.groupId = { $exists: false };
2380
+ }
2381
+ let getPriceInfo = await basePricingService.findOne( priceQuery, { standard: 1, step: 1 } );
2364
2382
  let findClient = await paymentService.findOneClient( { clientId: req.body.clientId } );
2365
2383
 
2366
2384
  console.log( getPriceInfo );
@@ -2370,8 +2388,10 @@ export const pricingListUpdate = async ( req, res ) => {
2370
2388
  pricingType: findClient.priceType,
2371
2389
  };
2372
2390
 
2373
- if ( findClient.priceType==='standard' ) {
2374
- getPriceInfo.standard.map( ( item ) => {
2391
+ // Old-data snapshot for the audit log. Guard on getPriceInfo: a brand-new
2392
+ // billing-group doc has none yet, and the no-doc path is handled below.
2393
+ if ( getPriceInfo && findClient.priceType==='standard' ) {
2394
+ ( getPriceInfo.standard || [] ).map( ( item ) => {
2375
2395
  oldData = {
2376
2396
  ...oldData,
2377
2397
  [item.productName+' '+'negotiatePrice']: item.negotiatePrice,
@@ -2383,8 +2403,8 @@ export const pricingListUpdate = async ( req, res ) => {
2383
2403
  };
2384
2404
  }
2385
2405
  } );
2386
- } else {
2387
- getPriceInfo.step.map( ( item ) => {
2406
+ } else if ( getPriceInfo ) {
2407
+ ( getPriceInfo.step || [] ).map( ( item ) => {
2388
2408
  oldData = {
2389
2409
  ...oldData,
2390
2410
  [item.productName+' '+'negotiatePrice']: item.negotiatePrice,
@@ -2496,6 +2516,13 @@ export const pricingListUpdate = async ( req, res ) => {
2496
2516
  if ( req.body.oneTimeFeePerStore != null && req.body.oneTimeFeePerStore !== '' ) {
2497
2517
  getPriceInfo.oneTimeFeePerStore = Number( req.body.oneTimeFeePerStore ) || 0;
2498
2518
  }
2519
+ // Keep the group identity on the doc when saving group-wise pricing.
2520
+ if ( req.body.groupId ) {
2521
+ getPriceInfo.groupId = req.body.groupId;
2522
+ if ( req.body.groupName ) {
2523
+ getPriceInfo.groupName = req.body.groupName;
2524
+ }
2525
+ }
2499
2526
  getPriceInfo.save().then( async () => {
2500
2527
  let clientDetails = await paymentService.findOne( { clientId: req.body.clientId }, { priceType: 1, paymentInvoice: 1, planDetails: 1 } );
2501
2528
  clientDetails.priceType = req.body.type;
@@ -2566,7 +2593,15 @@ export const pricingListUpdate = async ( req, res ) => {
2566
2593
 
2567
2594
  async function updatePricing( req, res, update ) {
2568
2595
  let baseProduct = await basePricingService.findOne( { clientId: { $exists: false } }, { basePricing: 1 } );
2569
- let getPriceInfo = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1 } );
2596
+ // Group-wise pricing: scope the doc to the group when a groupId is sent;
2597
+ // otherwise the brand-level doc (groupId unset).
2598
+ const pricingDocQuery = { clientId: req.body.clientId };
2599
+ if ( req.body.groupId ) {
2600
+ pricingDocQuery.groupId = req.body.groupId;
2601
+ } else {
2602
+ pricingDocQuery.groupId = { $exists: false };
2603
+ }
2604
+ let getPriceInfo = await basePricingService.findOne( pricingDocQuery, { standard: 1, step: 1 } );
2570
2605
  let clientDetails = await paymentService.findOne( { clientId: req.body.clientId } );
2571
2606
  if ( clientDetails ) {
2572
2607
  let products = clientDetails.planDetails.product.map( ( item ) => item.productName );
@@ -2637,12 +2672,20 @@ async function updatePricing( req, res, update ) {
2637
2672
  step: stepList,
2638
2673
  clientId: req.body.clientId,
2639
2674
  };
2675
+ // Stamp the group identity so group-wise pricing docs are distinguishable
2676
+ // from the brand-level doc.
2677
+ if ( req.body.groupId ) {
2678
+ data.groupId = req.body.groupId;
2679
+ if ( req.body.groupName ) {
2680
+ data.groupName = req.body.groupName;
2681
+ }
2682
+ }
2640
2683
  console.log( '🚀 ~ updatePricing ~ data:', data );
2641
2684
  if ( !getPriceInfo ) {
2642
2685
  await basePricingService.create( data );
2643
2686
  } else {
2644
2687
  delete data.clientId;
2645
- await basePricingService.updateOne( { clientId: req.body.clientId }, data );
2688
+ await basePricingService.updateOne( pricingDocQuery, data );
2646
2689
  }
2647
2690
  let product = [];
2648
2691
  let clientId = req.body.clientId;
@@ -4235,3 +4278,20 @@ export async function getClientDocuments( req, res ) {
4235
4278
  }
4236
4279
  }
4237
4280
 
4281
+ // Toggle billing-group-wise pricing on the client. When enabled, pricing is
4282
+ // maintained per billing group (separate basepricing docs keyed by groupId).
4283
+ export async function setBillingGroupWisePricing( req, res ) {
4284
+ try {
4285
+ const clientId = String( req.body?.clientId || '' );
4286
+ const enabled = req.body?.enabled === true || req.body?.enabled === 'true';
4287
+ if ( !clientId ) {
4288
+ return res.sendError( 'clientId is required', 400 );
4289
+ }
4290
+ await paymentService.updateOne( { clientId }, { billingGroupWisePricing: enabled } );
4291
+ return res.sendSuccess( { clientId, billingGroupWisePricing: enabled } );
4292
+ } catch ( error ) {
4293
+ logger.error( { error: error, function: 'setBillingGroupWisePricing' } );
4294
+ return res.sendError( error, 500 );
4295
+ }
4296
+ }
4297
+
@@ -0,0 +1,171 @@
1
+ import * as purchaseOrderService from '../services/purchaseOrder.service.js';
2
+ import { logger, insertOpenSearchData, fileUpload, customSignedUrl } from 'tango-app-api-middleware';
3
+
4
+ // Derive PO status from how much of it has been consumed.
5
+ function statusFor( totalAmount, remainingAmount ) {
6
+ if ( remainingAmount <= 0 ) {
7
+ return 'fullyUsed';
8
+ }
9
+ if ( remainingAmount < totalAmount ) {
10
+ return 'partiallyUsed';
11
+ }
12
+ return 'open';
13
+ }
14
+
15
+ function logPO( req, clientId, subType, changes ) {
16
+ try {
17
+ const logObj = {
18
+ userName: req.user?.userName,
19
+ email: req.user?.email,
20
+ clientId: clientId,
21
+ logSubType: subType,
22
+ logType: 'purchaseOrder',
23
+ date: new Date(),
24
+ changes: changes,
25
+ eventType: '',
26
+ timestamp: new Date(),
27
+ showTo: [ 'tango' ],
28
+ };
29
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, logObj );
30
+ } catch ( e ) {
31
+ logger.error( { error: e, function: 'logPO' } );
32
+ }
33
+ }
34
+
35
+ // Create a purchase order. remainingAmount starts equal to totalAmount.
36
+ export async function createPurchaseOrder( req, res ) {
37
+ try {
38
+ const clientId = String( req.body?.clientId || '' );
39
+ const companyName = String( req.body?.companyName || '' ).trim();
40
+ const purchaseOrderNumber = String( req.body?.purchaseOrderNumber || '' ).trim();
41
+ const totalAmount = Number( req.body?.totalAmount ) || 0;
42
+ const date = req.body?.date ? new Date( req.body.date ) : new Date();
43
+
44
+ if ( !clientId ) {
45
+ return res.sendError( 'clientId is required', 400 );
46
+ }
47
+ if ( !purchaseOrderNumber ) {
48
+ return res.sendError( 'purchaseOrderNumber is required', 400 );
49
+ }
50
+ if ( !( totalAmount > 0 ) ) {
51
+ return res.sendError( 'totalAmount must be greater than 0', 400 );
52
+ }
53
+
54
+ // Prevent duplicate PO numbers for the same client.
55
+ const existing = await purchaseOrderService.findOne( { clientId, purchaseOrderNumber } );
56
+ if ( existing ) {
57
+ return res.sendError( 'A purchase order with this number already exists', 409 );
58
+ }
59
+
60
+ // Optional PO PDF upload (multer single 'file') → assets bucket, same as
61
+ // brand document upload. PDF only.
62
+ let pdfPath = '';
63
+ const file = req.file;
64
+ if ( file ) {
65
+ if ( file.mimetype !== 'application/pdf' ) {
66
+ return res.sendError( 'Only PDF files are allowed', 400 );
67
+ }
68
+ const bucket = JSON.parse( process.env.BUCKET );
69
+ const safeName = purchaseOrderNumber.replace( /[^a-zA-Z0-9-_]/g, '_' );
70
+ const fileName = `${safeName}_${Date.now()}.pdf`;
71
+ const key = `${clientId}/purchase-orders/`;
72
+ let result = await fileUpload( {
73
+ Bucket: bucket.assets,
74
+ Key: key,
75
+ fileName: fileName,
76
+ ContentType: file.mimetype,
77
+ body: file.buffer,
78
+ } );
79
+ pdfPath = `${key}${fileName}`;
80
+ console.log( '🚀 ~ createPurchaseOrder ~ result:', result );
81
+ }
82
+
83
+ const created = await purchaseOrderService.create( {
84
+ clientId,
85
+ companyName,
86
+ purchaseOrderNumber,
87
+ totalAmount,
88
+ remainingAmount: totalAmount,
89
+ date,
90
+ status: 'open',
91
+ usage: [],
92
+ pdfPath,
93
+ createdBy: req.user?.email || req.user?.userName || '',
94
+ } );
95
+
96
+ logPO( req, clientId, 'purchaseOrderCreated', [ `PO ${purchaseOrderNumber} created with amount ${totalAmount}` ] );
97
+ return res.sendSuccess( created );
98
+ } catch ( error ) {
99
+ logger.error( { error: error, function: 'createPurchaseOrder' } );
100
+ return res.sendError( error, 500 );
101
+ }
102
+ }
103
+
104
+ // List a client's purchase orders (newest first).
105
+ export async function getPurchaseOrders( req, res ) {
106
+ try {
107
+ const clientId = String( req.query?.clientId || req.params?.clientId || '' );
108
+ if ( !clientId ) {
109
+ return res.sendError( 'clientId is required', 400 );
110
+ }
111
+ const orders = await purchaseOrderService.find( { clientId } );
112
+ const sorted = ( orders || [] ).sort( ( a, b ) =>
113
+ new Date( b.createdAt || b.date || 0 ) - new Date( a.createdAt || a.date || 0 ) );
114
+ // Attach a signed URL for any uploaded PO PDF (assets bucket).
115
+ const bucket = JSON.parse( process.env.BUCKET );
116
+ const withUrls = await Promise.all( sorted.map( async ( o ) => {
117
+ const plain = o.toObject?.() || o;
118
+ let pdfUrl = '';
119
+ if ( plain.pdfPath ) {
120
+ try {
121
+ pdfUrl = await customSignedUrl( { Bucket: bucket.assets, file_path: plain.pdfPath }, 8 );
122
+ } catch ( e ) {
123
+ pdfUrl = '';
124
+ }
125
+ }
126
+ return { ...plain, pdfUrl };
127
+ } ) );
128
+ return res.sendSuccess( { purchaseOrders: withUrls } );
129
+ } catch ( error ) {
130
+ logger.error( { error: error, function: 'getPurchaseOrders' } );
131
+ return res.sendError( error, 500 );
132
+ }
133
+ }
134
+
135
+ // Map an invoice to a PO: deduct the invoice amount from the PO's remaining
136
+ // balance, record the usage, update status, and log it. Shared with invoice
137
+ // generation/edit so the deduction happens wherever an invoice gets a PO.
138
+ // Returns the updated PO doc, or throws on a missing PO.
139
+ export async function applyInvoiceToPurchaseOrder( { clientId, purchaseOrderNumber, invoice, amount, req } ) {
140
+ if ( !purchaseOrderNumber ) {
141
+ return null;
142
+ }
143
+ const po = await purchaseOrderService.findOne( { clientId, purchaseOrderNumber } );
144
+ if ( !po ) {
145
+ logger.error( { function: 'applyInvoiceToPurchaseOrder', message: 'PO not found', clientId, purchaseOrderNumber } );
146
+ return null;
147
+ }
148
+ // Idempotency: if this invoice is already mapped to this PO, don't double-deduct.
149
+ const alreadyMapped = ( po.usage || [] ).some( ( u ) => u.invoice === invoice );
150
+ if ( alreadyMapped ) {
151
+ return po;
152
+ }
153
+ const amt = Number( amount ) || 0;
154
+ const remainingAmount = Math.round( ( ( po.remainingAmount || 0 ) - amt ) * 100 ) / 100;
155
+ const usage = ( po.usage || [] ).concat( [ {
156
+ invoice,
157
+ amount: amt,
158
+ mappedAt: new Date(),
159
+ mappedBy: req?.user?.email || req?.user?.userName || '',
160
+ } ] );
161
+ const status = statusFor( po.totalAmount || 0, remainingAmount );
162
+ await purchaseOrderService.updateOne(
163
+ { clientId, purchaseOrderNumber },
164
+ { remainingAmount, usage, status },
165
+ );
166
+ if ( req ) {
167
+ logPO( req, clientId, 'purchaseOrderInvoiceMapped',
168
+ [ `Invoice ${invoice} mapped to PO ${purchaseOrderNumber}; ${amt} deducted, remaining ${remainingAmount}` ] );
169
+ }
170
+ return { ...po.toObject?.() || po, remainingAmount, usage, status };
171
+ }
@@ -154,6 +154,10 @@ export const validatePriceSchema = joi.object( {
154
154
  clientId: joi.string().required(),
155
155
  products: joi.array().optional(),
156
156
  oneTimeFeePerStore: joi.number().optional().allow( null, '' ),
157
+ // Billing-group-wise pricing: when present, the pricing is saved to / read
158
+ // from the group's own basepricing doc instead of the brand-level one.
159
+ groupId: joi.string().optional().allow( null, '' ),
160
+ groupName: joi.string().optional().allow( null, '' ),
157
161
  pricing: joi.array().items( joi.object( {
158
162
  productName: joi.string().required(),
159
163
  negotiatePrice: joi.number().required(),
@@ -178,6 +182,8 @@ export const revisedParams = {
178
182
  export const validatePriceListSchema = joi.object( {
179
183
  priceType: joi.string().required(),
180
184
  clientId: joi.string().required(),
185
+ // Optional billing group filter for group-wise pricing.
186
+ groupId: joi.string().optional().allow( null, '' ),
181
187
  } );
182
188
 
183
189
  export const validatePriceListParams = {
@@ -1,8 +1,11 @@
1
1
  import express from 'express';
2
+ import multer from 'multer';
2
3
  export const billingRouter = express.Router();
4
+ const poUpload = multer( { storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } } );
3
5
  import { accessVerification, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
4
6
  import { getPaymentReminder, savePaymentReminder } from '../controllers/paymentReminder.controller.js';
5
7
  import { triggerPaymentReminders } from '../controllers/paymentReminderTrigger.controller.js';
8
+ import { createPurchaseOrder, getPurchaseOrders } from '../controllers/purchaseOrder.controller.js';
6
9
  import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, getBaseProducts, onetimePayment, subscribedStoreList, updateBillingGroup, gstinLookup } from '../controllers/billing.controllers.js';
7
10
  import { billingGroupSchema, clientProductsValid, createBillingGroupsSchema, deleteBillingGroupsSchema, getBillingGroupsSchema, getInvoiceSchema, leadProductsValid, onetimeFeeValid, subscribedStoreListSchema, updateBillingGroupsSchema } from '../dtos/validation.dtos.js';
8
11
 
@@ -37,6 +40,10 @@ billingRouter.get( '/gst-lookup/:gstin', isAllowedSessionHandler, accessVerifica
37
40
  billingRouter.get( '/payment-reminder/:clientId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), getPaymentReminder );
38
41
  billingRouter.post( '/payment-reminder', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), savePaymentReminder );
39
42
 
43
+ // Purchase Orders (brand-view Purchase Order tab).
44
+ billingRouter.get( '/purchase-orders', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), getPurchaseOrders );
45
+ billingRouter.post( '/purchase-orders', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), poUpload.single( 'file' ), createPurchaseOrder );
46
+
40
47
  // Cron-triggered: sends the configured payment reminder emails. Unauthenticated
41
48
  // like the other cron endpoints; protect at the network / scheduler layer.
42
49
  billingRouter.post( '/payment-reminder/trigger', triggerPaymentReminders );
@@ -1,6 +1,6 @@
1
1
 
2
2
  import express from 'express';
3
- import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts } from '../controllers/brandsBilling.controller.js';
3
+ import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts, additionalProductExport } from '../controllers/brandsBilling.controller.js';
4
4
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
5
5
 
6
6
  export const brandsBillingRouter = express.Router();
@@ -17,3 +17,4 @@ brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler
17
17
  brandsBillingRouter.post( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
18
18
  brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
19
19
  brandsBillingRouter.get( '/additionalProducts', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), additionalProducts );
20
+ brandsBillingRouter.get( '/additionalProductExport', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), additionalProductExport );
@@ -155,4 +155,7 @@ paymentSubscriptionRouter.post( '/createDefaultbillings', paymentController.crea
155
155
  paymentSubscriptionRouter.post( '/client-document/upload', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [ 'isEdit' ] } ] } ), documentUpload.single( 'file' ), paymentController.uploadClientDocument );
156
156
  paymentSubscriptionRouter.get( '/client-document/list', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [] } ] } ), paymentController.getClientDocuments );
157
157
 
158
+ // Toggle billing-group-wise pricing for a client (tango only, edit perm).
159
+ paymentSubscriptionRouter.post( '/billingGroupWisePricing', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [ 'isEdit' ] } ] } ), paymentController.setBillingGroupWisePricing );
160
+
158
161
 
@@ -0,0 +1,21 @@
1
+ import model from 'tango-api-schema';
2
+
3
+ export const find = ( query = {}, record = {} ) => {
4
+ return model.purchaseOrderModel.find( query, record );
5
+ };
6
+
7
+ export const findOne = ( query = {}, record = {} ) => {
8
+ return model.purchaseOrderModel.findOne( query, record );
9
+ };
10
+
11
+ export const create = ( record ) => {
12
+ return model.purchaseOrderModel.create( record );
13
+ };
14
+
15
+ export const updateOne = ( query, record ) => {
16
+ return model.purchaseOrderModel.updateOne( query, { $set: record } );
17
+ };
18
+
19
+ export const aggregate = ( query = [] ) => {
20
+ return model.purchaseOrderModel.aggregate( query );
21
+ };