tango-app-api-payment-subscription 3.5.15 → 3.5.17
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 +360 -0
- package/src/controllers/invoice.controller.js +212 -267
- package/src/controllers/paymentSubscription.controllers.js +183 -18
- package/src/dtos/validation.dtos.js +7 -0
- package/src/routes/brandsBilling.routes.js +2 -1
- package/src/routes/paymentSubscription.routes.js +12 -0
- package/src/services/clientPayment.services.js +5 -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.17",
|
|
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.35",
|
|
33
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
34
34
|
"winston": "^3.12.0",
|
|
35
35
|
"winston-daily-rotate-file": "^5.0.0",
|
|
@@ -582,6 +582,11 @@ export async function brandInvoiceList( req, res ) {
|
|
|
582
582
|
let summary = {
|
|
583
583
|
totalInvoices: allInvoices.length,
|
|
584
584
|
totalInvoiced: allInvoices.reduce( ( sum, inv ) => sum + ( inv.totalAmount || 0 ), 0 ),
|
|
585
|
+
// Footer totals over the FULL filtered set (not just the current page):
|
|
586
|
+
// stores, amount excl. GST and amount incl. GST.
|
|
587
|
+
totalStores: allInvoices.reduce( ( sum, inv ) => sum + ( inv.stores || 0 ), 0 ),
|
|
588
|
+
totalAmountExclGst: allInvoices.reduce( ( sum, inv ) => sum + ( inv.amount || 0 ), 0 ),
|
|
589
|
+
totalAmountInclGst: allInvoices.reduce( ( sum, inv ) => sum + ( inv.totalAmount || 0 ), 0 ),
|
|
585
590
|
pendingApproval: allInvoices.filter( ( inv ) => [ 'pendingCsm', 'pendingFinance', 'pendingApproval' ].includes( inv.status ) ).length,
|
|
586
591
|
pendingPayment: allInvoices.filter( ( inv ) => inv.paymentStatus === 'unpaid' && inv.status === 'approved' ).length,
|
|
587
592
|
paid: allInvoices.filter( ( inv ) => inv.paymentStatus === 'paid' ).length,
|
|
@@ -891,6 +896,32 @@ export async function latestDailyPricing( req, res ) {
|
|
|
891
896
|
} },
|
|
892
897
|
] );
|
|
893
898
|
|
|
899
|
+
// Newly onboarded stores = stores whose FIRST FILE date falls in the current
|
|
900
|
+
// calendar month. Counted over the FULL store list (record.stores), not the
|
|
901
|
+
// status/search-filtered view, so the card reflects all new onboardings.
|
|
902
|
+
// First-file date is edgefirstFileDate, with processfirstFileDate as the
|
|
903
|
+
// fallback (same rule the annexure uses); some legacy rows store it as a
|
|
904
|
+
// string, so coerce defensively.
|
|
905
|
+
const monthStart = dayjs().startOf( 'month' );
|
|
906
|
+
const monthEnd = dayjs().endOf( 'month' );
|
|
907
|
+
const newlyOnboardedStoreList = ( record.stores || [] )
|
|
908
|
+
.map( ( s ) => {
|
|
909
|
+
const raw = s.edgefirstFileDate || s.processfirstFileDate;
|
|
910
|
+
const ff = raw ? dayjs( raw ) : null;
|
|
911
|
+
return { store: s, ff };
|
|
912
|
+
} )
|
|
913
|
+
.filter( ( x ) => x.ff && x.ff.isValid() &&
|
|
914
|
+
x.ff.isAfter( monthStart.subtract( 1, 'millisecond' ) ) &&
|
|
915
|
+
x.ff.isBefore( monthEnd.add( 1, 'millisecond' ) ) )
|
|
916
|
+
.map( ( x ) => ( {
|
|
917
|
+
storeId: x.store.storeId,
|
|
918
|
+
storeName: x.store.storeName,
|
|
919
|
+
status: x.store.status,
|
|
920
|
+
firstFileDate: x.ff.format( 'YYYY-MM-DD' ),
|
|
921
|
+
} ) )
|
|
922
|
+
.sort( ( a, b ) => a.firstFileDate.localeCompare( b.firstFileDate ) );
|
|
923
|
+
const newlyOnboardedStores = newlyOnboardedStoreList.length;
|
|
924
|
+
|
|
894
925
|
let data = {
|
|
895
926
|
clientId: record.clientId,
|
|
896
927
|
brandName: record.brandName,
|
|
@@ -901,6 +932,8 @@ export async function latestDailyPricing( req, res ) {
|
|
|
901
932
|
status: record.status,
|
|
902
933
|
proRate: record.proRate,
|
|
903
934
|
count,
|
|
935
|
+
newlyOnboardedStores,
|
|
936
|
+
newlyOnboardedStoreList,
|
|
904
937
|
data: storeList,
|
|
905
938
|
monthlyBillingSummary,
|
|
906
939
|
};
|
|
@@ -2237,3 +2270,330 @@ export async function additionalProducts( req, res ) {
|
|
|
2237
2270
|
return res.sendError( error, 500 );
|
|
2238
2271
|
}
|
|
2239
2272
|
}
|
|
2273
|
+
|
|
2274
|
+
// POST a JSON payload to a Lambda Function URL and return the parsed JSON
|
|
2275
|
+
// (or false on failure). Node's fetch forbids a body on GET, so POST is used.
|
|
2276
|
+
async function lamdaServiceCall( url, data ) {
|
|
2277
|
+
try {
|
|
2278
|
+
const response = await fetch( url, {
|
|
2279
|
+
method: 'POST',
|
|
2280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2281
|
+
body: JSON.stringify( data ),
|
|
2282
|
+
signal: AbortSignal.timeout( 300000 ),
|
|
2283
|
+
} );
|
|
2284
|
+
if ( !response.ok ) {
|
|
2285
|
+
throw new Error( `Response status: ${response.status}` );
|
|
2286
|
+
}
|
|
2287
|
+
return await response.json();
|
|
2288
|
+
} catch ( error ) {
|
|
2289
|
+
logger.error( { error: error, message: data, function: 'lamdaServiceCall' } );
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Unit price for a product from the latest billing_details OpenSearch record.
|
|
2295
|
+
// productName match is case-insensitive. Returns 0 if not found / unavailable.
|
|
2296
|
+
async function billingDetailsPrice( clientId, productName ) {
|
|
2297
|
+
try {
|
|
2298
|
+
const osRes = await getOpenSearchData( BILLING_DETAILS_INDEX, {
|
|
2299
|
+
size: 1,
|
|
2300
|
+
query: { term: { 'client_id': clientId } },
|
|
2301
|
+
sort: [ { date_string: { order: 'desc' } } ],
|
|
2302
|
+
} );
|
|
2303
|
+
const hits = osRes?.body?.hits?.hits || osRes?.hits?.hits || [];
|
|
2304
|
+
const match = ( hits[0]?._source?.products || [] )
|
|
2305
|
+
.find( ( p ) => String( p.productName ).toLowerCase() === String( productName ).toLowerCase() );
|
|
2306
|
+
return Number( match?.price ) || 0;
|
|
2307
|
+
} catch ( osErr ) {
|
|
2308
|
+
logger.error( { error: osErr, function: 'billingDetailsPrice', clientId, productName } );
|
|
2309
|
+
return 0;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// Build the Lambda-backed export rows (VMS / Run AI / etc.). Calls the product's
|
|
2314
|
+
// Lambda for store_names (storeIds), maps them to the stores collection for
|
|
2315
|
+
// name + country (Zone), with current-month days and the billing_details price.
|
|
2316
|
+
async function lambdaStoreExport( clientId, lambdaUrl, billingProductName ) {
|
|
2317
|
+
const toDate = dayjs().format( 'DD-MM-YYYY' );
|
|
2318
|
+
const lambdaResult = await lamdaServiceCall( lambdaUrl, { to_date: toDate, client_id: clientId } );
|
|
2319
|
+
console.log( '🚀 ~ lambdaStoreExport ~ lambdaResult:', lambdaResult );
|
|
2320
|
+
// These Lambdas return the storeIds under either `store_ids` (Run AI) or
|
|
2321
|
+
// `store_names` (VMS) with no status_code. Accept whichever is present; data
|
|
2322
|
+
// availability = a non-empty array.
|
|
2323
|
+
const rawIds = ( lambdaResult && Array.isArray( lambdaResult.store_ids ) ) ? lambdaResult.store_ids :
|
|
2324
|
+
( ( lambdaResult && Array.isArray( lambdaResult.store_names ) ) ? lambdaResult.store_names : [] );
|
|
2325
|
+
const storeIds = rawIds.map( ( s ) => String( s ) );
|
|
2326
|
+
if ( !storeIds.length ) {
|
|
2327
|
+
return [];
|
|
2328
|
+
}
|
|
2329
|
+
const price = await billingDetailsPrice( clientId, billingProductName );
|
|
2330
|
+
const daysInMonth = dayjs().daysInMonth();
|
|
2331
|
+
const stores = await storeService.find(
|
|
2332
|
+
{ clientId: clientId, storeId: { $in: storeIds } },
|
|
2333
|
+
{ 'storeId': 1, 'storeName': 1, 'storeProfile.country': 1 },
|
|
2334
|
+
);
|
|
2335
|
+
const byId = new Map( stores.map( ( s ) => [ String( s.storeId ), s ] ) );
|
|
2336
|
+
return storeIds.map( ( id ) => {
|
|
2337
|
+
const s = byId.get( String( id ) );
|
|
2338
|
+
return {
|
|
2339
|
+
'Store Names': s?.storeName || id,
|
|
2340
|
+
'No of Days to be billed': daysInMonth,
|
|
2341
|
+
'Final Amount': price,
|
|
2342
|
+
'Zone': s?.storeProfile?.country || '',
|
|
2343
|
+
};
|
|
2344
|
+
} );
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// tangoTraffic working days per store, from the latest daily-pricing doc.
|
|
2348
|
+
// Map<storeId, workingdays>; stores with no tangoTraffic record are absent.
|
|
2349
|
+
async function tangoTrafficDaysByStore( clientId ) {
|
|
2350
|
+
const rows = await dailyPriceService.aggregate( [
|
|
2351
|
+
{ $match: { clientId: clientId } },
|
|
2352
|
+
{ $sort: { dateISO: -1 } },
|
|
2353
|
+
{ $limit: 1 },
|
|
2354
|
+
{ $unwind: '$stores' },
|
|
2355
|
+
{ $unwind: '$stores.products' },
|
|
2356
|
+
{ $match: { 'stores.products.productName': 'tangoTraffic' } },
|
|
2357
|
+
{ $group: { _id: '$stores.storeId', days: { $max: '$stores.products.workingdays' } } },
|
|
2358
|
+
] );
|
|
2359
|
+
const map = new Map();
|
|
2360
|
+
for ( const r of rows ) {
|
|
2361
|
+
map.set( String( r._id ), r.days || 0 );
|
|
2362
|
+
}
|
|
2363
|
+
return map;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Per-product detailed Excel export for the Additional Products section.
|
|
2367
|
+
// product selected via req.body.product / req.query.product. Eyetest is the
|
|
2368
|
+
// first; more products plug into the switch as their queries arrive.
|
|
2369
|
+
export async function additionalProductExport( req, res ) {
|
|
2370
|
+
try {
|
|
2371
|
+
const clientId = String( req.body?.clientId ?? req.query?.clientId ?? '' );
|
|
2372
|
+
const product = String( req.body?.product ?? req.query?.product ?? '' ).toLowerCase();
|
|
2373
|
+
// Optional date (YYYY-MM-DD) passed from the UI; defaults to today. Used by
|
|
2374
|
+
// products that report by a specific day (e.g. Remote Optum).
|
|
2375
|
+
const reqDate = String( req.body?.date ?? req.query?.date ?? '' ) || dayjs().format( 'YYYY-MM-DD' );
|
|
2376
|
+
if ( !clientId ) {
|
|
2377
|
+
return res.sendError( 'clientId is required', 400 );
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
if ( product === 'eyetest' ) {
|
|
2381
|
+
// Eye-test streams joined to their store, grouped per store to a stream
|
|
2382
|
+
// count; enriched with store name, country (Zone) and the store's
|
|
2383
|
+
// tangoTraffic working days (No of Days to be billed).
|
|
2384
|
+
const streamRows = await cameraService.aggregate( [
|
|
2385
|
+
{ $match: { $and: [
|
|
2386
|
+
{ clientId: clientId },
|
|
2387
|
+
{ isEyeTestStream: true },
|
|
2388
|
+
{ qrCode: { $exists: true } },
|
|
2389
|
+
] } },
|
|
2390
|
+
{ $lookup: {
|
|
2391
|
+
from: 'stores',
|
|
2392
|
+
let: { storeId: '$storeId' },
|
|
2393
|
+
pipeline: [ { $match: { $expr: { $and: [ { $eq: [ '$storeId', '$$storeId' ] } ] } } } ],
|
|
2394
|
+
as: 'stores',
|
|
2395
|
+
} },
|
|
2396
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: true } },
|
|
2397
|
+
{ $group: {
|
|
2398
|
+
_id: '$storeId',
|
|
2399
|
+
streamCount: { $sum: 1 },
|
|
2400
|
+
storeName: { $first: '$stores.storeName' },
|
|
2401
|
+
country: { $first: '$stores.storeProfile.country' },
|
|
2402
|
+
} },
|
|
2403
|
+
{ $sort: { storeName: 1 } },
|
|
2404
|
+
] );
|
|
2405
|
+
|
|
2406
|
+
const daysMap = await tangoTrafficDaysByStore( clientId );
|
|
2407
|
+
const price = ADDITIONAL_PRODUCT_PRICES.eyetest;
|
|
2408
|
+
const exportData = streamRows.map( ( r ) => {
|
|
2409
|
+
const streamCount = r.streamCount || 0;
|
|
2410
|
+
const days = daysMap.get( String( r._id ) ) || 0;
|
|
2411
|
+
// Amount = streams x price (no proration).
|
|
2412
|
+
return {
|
|
2413
|
+
'storeId': r._id,
|
|
2414
|
+
'storeName': r.storeName || '',
|
|
2415
|
+
'Stream Count': streamCount,
|
|
2416
|
+
'No of Days to be billed': days,
|
|
2417
|
+
'Amount': streamCount * price,
|
|
2418
|
+
'Zone': r.country || '',
|
|
2419
|
+
};
|
|
2420
|
+
} );
|
|
2421
|
+
|
|
2422
|
+
if ( !exportData.length ) {
|
|
2423
|
+
return res.sendError( 'No data', 204 );
|
|
2424
|
+
}
|
|
2425
|
+
await download( exportData, res );
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
if ( product === 'planogram' ) {
|
|
2430
|
+
// Unique storeName from the planograms collection, enriched with the
|
|
2431
|
+
// store's country (Zone) and tangoTraffic working days. Amount is the
|
|
2432
|
+
// flat planogram price per store.
|
|
2433
|
+
const planoRows = await planogramService.aggregate( [
|
|
2434
|
+
{ $match: { clientId: clientId } },
|
|
2435
|
+
{ $group: { _id: '$storeName', storeId: { $first: '$storeId' } } },
|
|
2436
|
+
{ $lookup: {
|
|
2437
|
+
from: 'stores',
|
|
2438
|
+
let: { sid: '$storeId' },
|
|
2439
|
+
pipeline: [ { $match: { $expr: { $eq: [ '$storeId', '$$sid' ] } } } ],
|
|
2440
|
+
as: 'store',
|
|
2441
|
+
} },
|
|
2442
|
+
{ $unwind: { path: '$store', preserveNullAndEmptyArrays: true } },
|
|
2443
|
+
{ $project: { _id: 0, storeName: '$_id', storeId: 1, country: '$store.storeProfile.country' } },
|
|
2444
|
+
{ $sort: { storeName: 1 } },
|
|
2445
|
+
] );
|
|
2446
|
+
|
|
2447
|
+
const price = ADDITIONAL_PRODUCT_PRICES.planogram;
|
|
2448
|
+
// Planogram bills the full current month — every store shows the number of
|
|
2449
|
+
// days in the current calendar month (not per-store working days).
|
|
2450
|
+
const daysInMonth = dayjs().daysInMonth();
|
|
2451
|
+
const exportData = planoRows.map( ( r ) => {
|
|
2452
|
+
// Amount = flat planogram price per store (no proration).
|
|
2453
|
+
return {
|
|
2454
|
+
'storeName': r.storeName || '',
|
|
2455
|
+
'No of Days to be billed': daysInMonth,
|
|
2456
|
+
'Amount': price,
|
|
2457
|
+
'Zone': r.country || '',
|
|
2458
|
+
};
|
|
2459
|
+
} );
|
|
2460
|
+
|
|
2461
|
+
if ( !exportData.length ) {
|
|
2462
|
+
return res.sendError( 'No data', 204 );
|
|
2463
|
+
}
|
|
2464
|
+
await download( exportData, res );
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
if ( product === 'aimanager' ) {
|
|
2469
|
+
// AI Manager bills the same stores as tangoTraffic running > 1 day. One
|
|
2470
|
+
// row per such store with its tangoTraffic working days (No of Days to be
|
|
2471
|
+
// billed), the flat AI Manager price, and the store's country (Zone).
|
|
2472
|
+
const aiRows = await dailyPriceService.aggregate( [
|
|
2473
|
+
{ $match: { clientId: clientId } },
|
|
2474
|
+
{ $sort: { dateISO: -1 } },
|
|
2475
|
+
{ $limit: 1 },
|
|
2476
|
+
{ $unwind: '$stores' },
|
|
2477
|
+
{ $unwind: '$stores.products' },
|
|
2478
|
+
{ $match: { 'stores.products.productName': 'tangoTraffic', 'stores.products.workingdays': { $gt: 1 } } },
|
|
2479
|
+
{ $group: {
|
|
2480
|
+
_id: '$stores.storeId',
|
|
2481
|
+
storeName: { $first: '$stores.storeName' },
|
|
2482
|
+
days: { $max: '$stores.products.workingdays' },
|
|
2483
|
+
} },
|
|
2484
|
+
{ $lookup: {
|
|
2485
|
+
from: 'stores',
|
|
2486
|
+
let: { sid: '$_id' },
|
|
2487
|
+
pipeline: [ { $match: { $expr: { $eq: [ '$storeId', '$$sid' ] } } } ],
|
|
2488
|
+
as: 'store',
|
|
2489
|
+
} },
|
|
2490
|
+
{ $unwind: { path: '$store', preserveNullAndEmptyArrays: true } },
|
|
2491
|
+
{ $project: { _id: 0, storeId: '$_id', storeName: 1, days: 1, country: '$store.storeProfile.country' } },
|
|
2492
|
+
{ $sort: { storeName: 1 } },
|
|
2493
|
+
] );
|
|
2494
|
+
|
|
2495
|
+
const price = ADDITIONAL_PRODUCT_PRICES.aiManager;
|
|
2496
|
+
const exportData = aiRows.map( ( r ) => {
|
|
2497
|
+
// Amount = flat AI Manager price per store (no proration).
|
|
2498
|
+
return {
|
|
2499
|
+
'Store Name': r.storeName || '',
|
|
2500
|
+
'No of Days to be billed': r.days || 0,
|
|
2501
|
+
'Final Amount': price,
|
|
2502
|
+
'Zone': r.country || '',
|
|
2503
|
+
};
|
|
2504
|
+
} );
|
|
2505
|
+
|
|
2506
|
+
if ( !exportData.length ) {
|
|
2507
|
+
return res.sendError( 'No data', 204 );
|
|
2508
|
+
}
|
|
2509
|
+
await download( exportData, res );
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
if ( product === 'vms' ) {
|
|
2514
|
+
const exportData = await lambdaStoreExport(
|
|
2515
|
+
clientId,
|
|
2516
|
+
'https://ppf3l3mxc2lorh5hkrsj6zwyim0bupxw.lambda-url.ap-south-1.on.aws/',
|
|
2517
|
+
'VMS',
|
|
2518
|
+
);
|
|
2519
|
+
if ( !exportData.length ) {
|
|
2520
|
+
return res.sendError( 'No data', 204 );
|
|
2521
|
+
}
|
|
2522
|
+
await download( exportData, res );
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
if ( product === 'runai' ) {
|
|
2527
|
+
const exportData = await lambdaStoreExport(
|
|
2528
|
+
clientId,
|
|
2529
|
+
'https://mdm3mf7wuficgv3jjspkws2nu40azlsc.lambda-url.ap-south-1.on.aws/',
|
|
2530
|
+
'Run AI',
|
|
2531
|
+
);
|
|
2532
|
+
console.log( '🚀 ~ additionalProductExport ~ exportData:', exportData );
|
|
2533
|
+
if ( !exportData.length ) {
|
|
2534
|
+
return res.sendError( 'No data', 204 );
|
|
2535
|
+
}
|
|
2536
|
+
await download( exportData, res );
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if ( product === 'remoteoptum' ) {
|
|
2541
|
+
// Detailed optom-audit export from the remote_optom_steps_summary index
|
|
2542
|
+
// for the given date (passed from the UI, defaults to today). Each record
|
|
2543
|
+
// is one row; the nested `steps` object is flattened into TRUE/FALSE
|
|
2544
|
+
// columns in a fixed (alphabetical) order.
|
|
2545
|
+
const STEP_COLUMNS = [
|
|
2546
|
+
'Adjust-Phoropter', 'DuoChrome-Test', 'Explanation', 'Final-Prescription',
|
|
2547
|
+
'Handover', 'History-Taking', 'JCC', 'Near-Vision', 'Personal-Intro',
|
|
2548
|
+
'Subjective-Refraction', 'Trial-Frame', 'VA-Check',
|
|
2549
|
+
];
|
|
2550
|
+
let optomHits = [];
|
|
2551
|
+
try {
|
|
2552
|
+
const osRes = await getOpenSearchData( 'remote_optom_steps_summary', {
|
|
2553
|
+
// A single day is well under the 10k window; pull up to the cap.
|
|
2554
|
+
// NOTE: do NOT sort on StartTime — it's an analyzed text field, and
|
|
2555
|
+
// sorting on it makes OpenSearch return ZERO hits. The day filter is
|
|
2556
|
+
// enough; rows come back in index order.
|
|
2557
|
+
size: 10000,
|
|
2558
|
+
query: { term: { 'Date': reqDate } },
|
|
2559
|
+
} );
|
|
2560
|
+
optomHits = osRes?.body?.hits?.hits || osRes?.hits?.hits || [];
|
|
2561
|
+
} catch ( osErr ) {
|
|
2562
|
+
logger.error( { error: osErr, function: 'additionalProductExport.remoteOptum', clientId } );
|
|
2563
|
+
return res.sendError( 'Failed to fetch Remote Optum data', 502 );
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
const exportData = optomHits.map( ( h ) => {
|
|
2567
|
+
const s = h._source || {};
|
|
2568
|
+
const steps = s.steps || {};
|
|
2569
|
+
const row = {
|
|
2570
|
+
'storeName': s.storeName || '',
|
|
2571
|
+
'engagementId': s.engagementId || '',
|
|
2572
|
+
'queue_id': s.queue_id || '',
|
|
2573
|
+
'optm_name': s.optm_name || '',
|
|
2574
|
+
'optm_id': s.optm_id || '',
|
|
2575
|
+
'optm_Emailid': s.optm_Emailid || '',
|
|
2576
|
+
'Date': s.Date || '',
|
|
2577
|
+
'StartTime': s.StartTime || '',
|
|
2578
|
+
'EndTime': s.EndTime || '',
|
|
2579
|
+
'Duration (min)': s['Duration (min)'] != null ? s['Duration (min)'] : '',
|
|
2580
|
+
};
|
|
2581
|
+
for ( const col of STEP_COLUMNS ) {
|
|
2582
|
+
row[col] = steps[col] === true ? 'TRUE' : 'FALSE';
|
|
2583
|
+
}
|
|
2584
|
+
return row;
|
|
2585
|
+
} );
|
|
2586
|
+
|
|
2587
|
+
if ( !exportData.length ) {
|
|
2588
|
+
return res.sendError( 'No data', 204 );
|
|
2589
|
+
}
|
|
2590
|
+
await download( exportData, res );
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
return res.sendError( `Unsupported product: ${product}`, 400 );
|
|
2595
|
+
} catch ( error ) {
|
|
2596
|
+
logger.error( { error: error, function: 'additionalProductExport' } );
|
|
2597
|
+
return res.sendError( error, 500 );
|
|
2598
|
+
}
|
|
2599
|
+
}
|