tango-app-api-payment-subscription 3.5.8 → 3.5.10
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/bankTransaction.controller.js +100 -51
- package/src/controllers/brandsBilling.controller.js +110 -21
- package/src/controllers/estimate.controller.js +141 -68
- package/src/controllers/invoice.controller.js +137 -6
- package/src/dtos/validation.dtos.js +3 -0
- package/src/hbs/estimateEmail.hbs +78 -0
- package/src/hbs/estimatePdf.hbs +1632 -118
- package/src/hbs/invoicePdf.hbs +37 -105
- package/src/routes/invoice.routes.js +4 -2
- package/src/services/estimate.service.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-payment-subscription",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.10",
|
|
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.29",
|
|
33
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
34
34
|
"winston": "^3.12.0",
|
|
35
35
|
"winston-daily-rotate-file": "^5.0.0",
|
|
@@ -467,69 +467,118 @@ export async function resolveBankTransaction( req, res ) {
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
// ---- reconcile ----
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
470
|
+
// Accept either a single invoiceId (legacy) or a list of invoiceIds. One
|
|
471
|
+
// payment can settle multiple invoices: the amount auto-fills them in the
|
|
472
|
+
// order given, each settled fully until the money runs out.
|
|
473
|
+
const invoiceIds = Array.isArray( req.body?.invoiceIds ) && req.body.invoiceIds.length ?
|
|
474
|
+
req.body.invoiceIds.map( String ) :
|
|
475
|
+
( req.body?.invoiceId ? [ String( req.body.invoiceId ) ] : [] );
|
|
476
|
+
if ( !invoiceIds.length ) {
|
|
477
|
+
return res.sendError( 'invoiceIds is required to reconcile', 400 );
|
|
477
478
|
}
|
|
478
479
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
480
|
+
// EDIT case: this transaction was ALREADY reconciled, so its amount was
|
|
481
|
+
// previously applied to one or more invoices. Reverse each prior allocation
|
|
482
|
+
// first so re-reconciling never double-counts. Prefer the recorded
|
|
483
|
+
// per-invoice allocations; fall back to the single invoiceId for older rows.
|
|
484
|
+
if ( txn.status === 'reconciled' ) {
|
|
485
|
+
const priorAllocations = ( Array.isArray( txn.appliedInvoices ) && txn.appliedInvoices.length ) ?
|
|
486
|
+
txn.appliedInvoices :
|
|
487
|
+
( txn.invoiceId ? [ { invoiceId: txn.invoiceId, amount: txn.amount } ] : [] );
|
|
488
|
+
for ( const alloc of priorAllocations ) {
|
|
489
|
+
const prevInvoice = await invoiceService.findOne( { _id: alloc.invoiceId } );
|
|
490
|
+
if ( !prevInvoice ) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const revertedPaid = round2( Math.max( 0, ( Number( prevInvoice.paidAmount ) || 0 ) - ( Number( alloc.amount ) || 0 ) ) );
|
|
494
|
+
const prevTotal = Number( prevInvoice.totalAmount ) || 0;
|
|
495
|
+
const revertedStatus = revertedPaid <= AMOUNT_TOLERANCE ? 'unpaid' :
|
|
496
|
+
( round2( prevTotal - revertedPaid ) > AMOUNT_TOLERANCE ? 'partial' : 'paid' );
|
|
497
|
+
await invoiceService.invoiceUpdateOne(
|
|
498
|
+
{ _id: prevInvoice._id },
|
|
499
|
+
{
|
|
500
|
+
$set: { paymentStatus: revertedStatus, paidAmount: revertedPaid },
|
|
501
|
+
$pull: { paymentHistory: { reference: txn.refNo || undefined, method: 'bank-statement' } },
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
}
|
|
495
505
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
506
|
+
|
|
507
|
+
// Apply the payment across the selected invoices in order (auto-fill).
|
|
508
|
+
let remaining = round2( txn.amount );
|
|
509
|
+
const appliedInvoices = [];
|
|
510
|
+
const settledLabels = [];
|
|
511
|
+
let firstInvoice = null;
|
|
512
|
+
for ( const id of invoiceIds ) {
|
|
513
|
+
const invoice = await invoiceService.findOne( { _id: id } );
|
|
514
|
+
if ( !invoice ) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if ( !firstInvoice ) {
|
|
518
|
+
firstInvoice = invoice;
|
|
519
|
+
}
|
|
520
|
+
const prevPaid = Number( invoice.paidAmount ) || 0;
|
|
521
|
+
const totalAmount = Number( invoice.totalAmount ) || 0;
|
|
522
|
+
const outstanding = round2( Math.max( 0, totalAmount - prevPaid ) );
|
|
523
|
+
const applied = round2( Math.max( 0, Math.min( remaining, outstanding ) ) );
|
|
524
|
+
if ( applied <= 0 && outstanding > 0 ) {
|
|
525
|
+
// No money left for this invoice — record it as selected but unfunded.
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
remaining = round2( remaining - applied );
|
|
529
|
+
const newPaid = round2( prevPaid + applied );
|
|
530
|
+
const fullySettled = round2( totalAmount - newPaid ) <= AMOUNT_TOLERANCE;
|
|
531
|
+
await invoiceService.invoiceUpdateOne(
|
|
532
|
+
{ _id: invoice._id },
|
|
533
|
+
{
|
|
534
|
+
$set: {
|
|
535
|
+
paymentStatus: fullySettled ? 'paid' : 'partial',
|
|
536
|
+
paidAmount: newPaid,
|
|
537
|
+
...( fullySettled ? { paidDate: new Date() } : {} ),
|
|
538
|
+
},
|
|
539
|
+
$push: {
|
|
540
|
+
paymentHistory: {
|
|
541
|
+
amount: applied,
|
|
542
|
+
date: txn.valueDate || new Date(),
|
|
543
|
+
method: 'bank-statement',
|
|
544
|
+
reference: txn.refNo || undefined,
|
|
545
|
+
notes: `Manually reconciled from "${txn.fileName || 'statement'}"`,
|
|
546
|
+
recordedBy: req.user?.email || req.user?.userName || 'bank-reconciliation',
|
|
547
|
+
recordedAt: new Date(),
|
|
548
|
+
},
|
|
515
549
|
},
|
|
516
550
|
},
|
|
517
|
-
|
|
518
|
-
|
|
551
|
+
);
|
|
552
|
+
appliedInvoices.push( { invoiceId: String( invoice._id ), invoice: invoice.invoice, amount: applied } );
|
|
553
|
+
settledLabels.push( `${invoice.invoice} (${fullySettled ? 'Paid' : 'Partial'})` );
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if ( !appliedInvoices.length ) {
|
|
557
|
+
return res.sendError( 'None of the selected invoices could be reconciled.', 400 );
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const pending = round2( remaining );
|
|
561
|
+
let note = appliedInvoices.length > 1 ?
|
|
562
|
+
`Reconciled across ${appliedInvoices.length} invoices: ${settledLabels.join( ', ' )}` :
|
|
563
|
+
`Manually reconciled to ${settledLabels[0]}`;
|
|
564
|
+
if ( pending > AMOUNT_TOLERANCE ) {
|
|
565
|
+
note += ` — ₹${pending} excess to follow up`;
|
|
566
|
+
}
|
|
519
567
|
|
|
520
|
-
// Brand name for the table's Brand Name column.
|
|
521
|
-
const client = await clientService.findOne( { clientId:
|
|
568
|
+
// Brand name for the table's Brand Name column (from the first invoice).
|
|
569
|
+
const client = await clientService.findOne( { clientId: firstInvoice.clientId }, { clientName: 1 } );
|
|
522
570
|
await bankTransactionService.updateOne( { _id: txnId }, { $set: {
|
|
523
571
|
status: 'reconciled',
|
|
524
572
|
contacted: false,
|
|
525
|
-
invoice: invoice.
|
|
526
|
-
invoiceId:
|
|
527
|
-
|
|
528
|
-
|
|
573
|
+
invoice: appliedInvoices.map( ( a ) => a.invoice ).join( ', ' ),
|
|
574
|
+
invoiceId: appliedInvoices[0].invoiceId,
|
|
575
|
+
appliedInvoices,
|
|
576
|
+
identifiedClientId: String( firstInvoice.clientId ?? '' ),
|
|
577
|
+
identifiedClientName: client?.clientName || firstInvoice.companyName || '',
|
|
529
578
|
resultNote: note,
|
|
530
579
|
} } );
|
|
531
580
|
|
|
532
|
-
return res.sendSuccess( { status: 'reconciled', note,
|
|
581
|
+
return res.sendSuccess( { status: 'reconciled', note, invoices: appliedInvoices.map( ( a ) => a.invoice ), pending } );
|
|
533
582
|
} catch ( error ) {
|
|
534
583
|
logger.error( { error: error, function: 'resolveBankTransaction' } );
|
|
535
584
|
return res.sendError( error, 500 );
|
|
@@ -30,11 +30,36 @@ export async function brandsBillingList( req, res ) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if ( req.body.paymentStatus && req.body.paymentStatus.length > 0 ) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
33
|
+
// Filter by the SAME derived buckets the pills use (see payBucketExpr),
|
|
34
|
+
// so a selected pill returns exactly the rows it counts:
|
|
35
|
+
// trial -> paymentStatus == 'trial'
|
|
36
|
+
// free -> paymentStatus == 'free'
|
|
37
|
+
// paid -> paymentStatus in {paid,unbilled,due} AND no trial product
|
|
38
|
+
// trialPaid -> paymentStatus in {paid,unbilled,due} AND has a trial product
|
|
39
|
+
// 'trialPaid' is a derived bucket — there is no stored 'trialPaid'
|
|
40
|
+
// status — which is why a plain $in match returned no data.
|
|
41
|
+
const buckets = req.body.paymentStatus;
|
|
42
|
+
const paidLike = [ 'paid', 'unbilled', 'due' ];
|
|
43
|
+
const hasTrialProduct = { $gt: [ { $size: { $filter: {
|
|
44
|
+
input: { $ifNull: [ '$planDetails.product', [] ] },
|
|
45
|
+
as: 'p',
|
|
46
|
+
cond: { $eq: [ '$$p.status', 'trial' ] },
|
|
47
|
+
} } }, 0 ] };
|
|
48
|
+
const ors = [];
|
|
49
|
+
for ( const b of buckets ) {
|
|
50
|
+
if ( b === 'trial' || b === 'free' ) {
|
|
51
|
+
ors.push( { $eq: [ '$planDetails.paymentStatus', b ] } );
|
|
52
|
+
} else if ( b === 'paid' ) {
|
|
53
|
+
ors.push( { $and: [ { $in: [ '$planDetails.paymentStatus', paidLike ] }, { $not: hasTrialProduct } ] } );
|
|
54
|
+
} else if ( b === 'trialPaid' ) {
|
|
55
|
+
ors.push( { $and: [ { $in: [ '$planDetails.paymentStatus', paidLike ] }, hasTrialProduct ] } );
|
|
56
|
+
} else {
|
|
57
|
+
ors.push( { $eq: [ '$planDetails.paymentStatus', b ] } );
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if ( ors.length ) {
|
|
61
|
+
query.push( { $match: { $expr: ors.length === 1 ? ors[0] : { $or: ors } } } );
|
|
62
|
+
}
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
query.push(
|
|
@@ -173,6 +198,21 @@ export async function brandsBillingList( req, res ) {
|
|
|
173
198
|
dueInr: { $ifNull: [ '$invoiceData.dueInr', 0 ] },
|
|
174
199
|
dueUsd: { $ifNull: [ '$invoiceData.dueUsd', 0 ] },
|
|
175
200
|
nextBillingDate: { $ifNull: [ '$billingData.nextBillingDate', null ] },
|
|
201
|
+
// Whether billing is SET UP — true when a billing group exists (or
|
|
202
|
+
// live products are configured). Drives the Setup-Billing vs View
|
|
203
|
+
// button; must NOT depend on the volatile current-month run count.
|
|
204
|
+
billingConfigured: {
|
|
205
|
+
$or: [
|
|
206
|
+
{ $ne: [ '$billingData', null ] },
|
|
207
|
+
{ $gt: [ {
|
|
208
|
+
$size: { $filter: {
|
|
209
|
+
input: { $ifNull: [ '$planDetails.product', [] ] },
|
|
210
|
+
as: 'prod',
|
|
211
|
+
cond: { $eq: [ '$$prod.status', 'live' ] },
|
|
212
|
+
} },
|
|
213
|
+
}, 0 ] },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
176
216
|
},
|
|
177
217
|
},
|
|
178
218
|
{
|
|
@@ -184,10 +224,25 @@ export async function brandsBillingList( req, res ) {
|
|
|
184
224
|
totalStores: 1,
|
|
185
225
|
billingStores: 1,
|
|
186
226
|
productsAdded: 1,
|
|
227
|
+
billingConfigured: 1,
|
|
187
228
|
dueInr: 1,
|
|
188
229
|
dueUsd: 1,
|
|
189
230
|
status: 1,
|
|
190
|
-
|
|
231
|
+
// Display mapping for the Payment Status column: an 'unbilled' or
|
|
232
|
+
// 'due' plan is shown as 'paid'; any other status is shown as-is.
|
|
233
|
+
paymentStatus: {
|
|
234
|
+
$cond: {
|
|
235
|
+
if: { $eq: [ '$planDetails.paymentStatus', 'unbilled' ] },
|
|
236
|
+
then: 'paid',
|
|
237
|
+
else: {
|
|
238
|
+
$cond: {
|
|
239
|
+
if: { $eq: [ '$planDetails.paymentStatus', 'due' ] },
|
|
240
|
+
then: 'paid',
|
|
241
|
+
else: '$planDetails.paymentStatus',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
191
246
|
nextBillingDate: 1,
|
|
192
247
|
currencyType: '$paymentInvoice.currencyType',
|
|
193
248
|
},
|
|
@@ -249,9 +304,11 @@ export async function brandsBillingList( req, res ) {
|
|
|
249
304
|
branches: [
|
|
250
305
|
{ case: { $eq: [ '$$ps', 'trial' ] }, then: 'trial' },
|
|
251
306
|
{ case: { $eq: [ '$$ps', 'free' ] }, then: 'free' },
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
307
|
+
// 'unbilled' and 'due' are treated as PAID (same mapping as the
|
|
308
|
+
// Payment Status column). A paid/unbilled/due plan that still has
|
|
309
|
+
// a product on trial is bucketed as trialPaid.
|
|
310
|
+
{ case: { $and: [ { $in: [ '$$ps', [ 'paid', 'unbilled', 'due' ] ] }, '$$hasTrialProduct' ] }, then: 'trialPaid' },
|
|
311
|
+
{ case: { $in: [ '$$ps', [ 'paid', 'unbilled', 'due' ] ] }, then: 'paid' },
|
|
255
312
|
],
|
|
256
313
|
default: 'other',
|
|
257
314
|
},
|
|
@@ -268,7 +325,7 @@ export async function brandsBillingList( req, res ) {
|
|
|
268
325
|
// regardless of the selected tab, and show even when the Active tab is
|
|
269
326
|
// empty.
|
|
270
327
|
const lifecycle = { active: 0, hold: 0, suspended: 0, deactive: 0 };
|
|
271
|
-
const payTotals = { trial: 0, paid: 0, free: 0, trialPaid: 0
|
|
328
|
+
const payTotals = { trial: 0, paid: 0, free: 0, trialPaid: 0 };
|
|
272
329
|
const paymentByStatus = {};
|
|
273
330
|
let totalBrands = 0;
|
|
274
331
|
matrixAgg.forEach( ( row ) => {
|
|
@@ -283,7 +340,7 @@ export async function brandsBillingList( req, res ) {
|
|
|
283
340
|
payTotals[pay] += n;
|
|
284
341
|
}
|
|
285
342
|
if ( !paymentByStatus[st] ) {
|
|
286
|
-
paymentByStatus[st] = { trial: 0, paid: 0, free: 0, trialPaid: 0
|
|
343
|
+
paymentByStatus[st] = { trial: 0, paid: 0, free: 0, trialPaid: 0 };
|
|
287
344
|
}
|
|
288
345
|
if ( paymentByStatus[st][pay] != null ) {
|
|
289
346
|
paymentByStatus[st][pay] += n;
|
|
@@ -300,7 +357,6 @@ export async function brandsBillingList( req, res ) {
|
|
|
300
357
|
paid: payTotals.paid,
|
|
301
358
|
free: payTotals.free,
|
|
302
359
|
trialPaid: payTotals.trialPaid,
|
|
303
|
-
unbilled: payTotals.unbilled,
|
|
304
360
|
paymentByStatus,
|
|
305
361
|
// Money/store totals stay tied to the filtered view so they match the
|
|
306
362
|
// rows on screen.
|
|
@@ -594,8 +650,14 @@ export async function latestDailyPricing( req, res ) {
|
|
|
594
650
|
let stores = record.stores || [];
|
|
595
651
|
let count = stores.length;
|
|
596
652
|
|
|
597
|
-
|
|
598
|
-
|
|
653
|
+
// statusFilter may be an array (multi-select) or a legacy string. An empty
|
|
654
|
+
// value / empty array means "all statuses".
|
|
655
|
+
const statusFilterList = Array.isArray( req.body.statusFilter ) ?
|
|
656
|
+
req.body.statusFilter.filter( Boolean ) :
|
|
657
|
+
( req.body.statusFilter ? [ req.body.statusFilter ] : [] );
|
|
658
|
+
if ( statusFilterList.length ) {
|
|
659
|
+
const allowed = new Set( statusFilterList );
|
|
660
|
+
stores = stores.filter( ( s ) => allowed.has( s.status ) );
|
|
599
661
|
}
|
|
600
662
|
|
|
601
663
|
if ( req.body.searchValue && req.body.searchValue !== '' ) {
|
|
@@ -1557,18 +1619,38 @@ export async function billingSummary( req, res ) {
|
|
|
1557
1619
|
} );
|
|
1558
1620
|
const clientById = new Map( clients.map( ( c ) => [ String( c.clientId ), c ] ) );
|
|
1559
1621
|
|
|
1622
|
+
// Registered Entity comes from the billings collection (each billing group's
|
|
1623
|
+
// registeredCompanyName). A client can have multiple groups/names, so
|
|
1624
|
+
// collect the distinct list per client — the UI shows the first and reveals
|
|
1625
|
+
// the rest on hover.
|
|
1626
|
+
const billingNameAgg = await billingService.aggregatebilling( [
|
|
1627
|
+
{ $match: { registeredCompanyName: { $exists: true, $nin: [ '', null ] } } },
|
|
1628
|
+
{ $group: { _id: '$clientId', names: { $addToSet: '$registeredCompanyName' } } },
|
|
1629
|
+
] );
|
|
1630
|
+
const regNamesByClient = new Map(
|
|
1631
|
+
billingNameAgg.map( ( b ) => [ String( b._id ), ( b.names || [] ).filter( Boolean ) ] ),
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1560
1634
|
// Per-client CSM (userAssignedStore, tangoUserType 'csm'); display the
|
|
1561
1635
|
// email's local part since the collection carries no display name.
|
|
1562
1636
|
const usdRate = await getUsdInrRate();
|
|
1563
1637
|
|
|
1564
|
-
// Current month's store count comes from dailyPricing
|
|
1565
|
-
//
|
|
1638
|
+
// Current month's store count comes from dailyPricing — counted the same
|
|
1639
|
+
// way as Brands & Billing's "Billing Stores": distinct ACTIVE stores that
|
|
1640
|
+
// RAN on more than one day in the month (a single-day appearance is
|
|
1641
|
+
// transient and isn't billed). Invoices for the running month usually don't
|
|
1642
|
+
// exist yet, so this is the current-month source.
|
|
1566
1643
|
const curMonthStart = new Date( now.startOf( 'month' ).toISOString() );
|
|
1567
1644
|
const latestDp = await dailyPriceService.aggregate( [
|
|
1568
1645
|
{ $match: { dateISO: { $gte: curMonthStart } } },
|
|
1569
|
-
{ $
|
|
1570
|
-
{ $
|
|
1571
|
-
{ $group: {
|
|
1646
|
+
{ $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
|
|
1647
|
+
{ $match: { 'stores.status': 'active' } },
|
|
1648
|
+
{ $group: {
|
|
1649
|
+
_id: { clientId: '$clientId', storeId: '$stores.storeId' },
|
|
1650
|
+
days: { $addToSet: { $dateToString: { format: '%Y-%m-%d', date: '$dateISO' } } },
|
|
1651
|
+
} },
|
|
1652
|
+
{ $match: { $expr: { $gt: [ { $size: '$days' }, 1 ] } } },
|
|
1653
|
+
{ $group: { _id: '$_id.clientId', stores: { $sum: 1 } } },
|
|
1572
1654
|
] );
|
|
1573
1655
|
const curStoresByClient = new Map( latestDp.map( ( d ) => [ String( d._id ), d.stores || 0 ] ) );
|
|
1574
1656
|
|
|
@@ -1762,10 +1844,17 @@ export async function billingSummary( req, res ) {
|
|
|
1762
1844
|
revCur = revCur == null ? null : Math.round( revCur );
|
|
1763
1845
|
const revPrevOut = revPrev == null ? null : Math.round( revPrev );
|
|
1764
1846
|
const installationOut = r.installationFee ? Math.round( r.installationFee ) : null;
|
|
1847
|
+
// Registered Entity from the billings collection (distinct group names).
|
|
1848
|
+
// First name shown in the column; the full list is sent so the UI can
|
|
1849
|
+
// reveal the others on hover. Fall back to the invoice company name when
|
|
1850
|
+
// a client has no billing-group registered name.
|
|
1851
|
+
const regNames = regNamesByClient.get( r.clientId ) || [];
|
|
1852
|
+
const registeredEntity = regNames[0] || r.registeredEntity || '';
|
|
1765
1853
|
return {
|
|
1766
1854
|
clientId: r.clientId,
|
|
1767
|
-
clientName: r.clientName ||
|
|
1768
|
-
registeredEntity
|
|
1855
|
+
clientName: r.clientName || registeredEntity || r.clientId,
|
|
1856
|
+
registeredEntity,
|
|
1857
|
+
registeredEntities: regNames.length ? regNames : ( r.registeredEntity ? [ r.registeredEntity ] : [] ),
|
|
1769
1858
|
status: r.status,
|
|
1770
1859
|
products: r.products,
|
|
1771
1860
|
csm: r.csm,
|