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.
@@ -286,6 +286,61 @@ export async function createInvoice( req, res ) {
286
286
  products = expanded;
287
287
  }
288
288
 
289
+ // One-time onboarding fee: stores in THIS billing group whose first file
290
+ // date falls in the invoice's billing month, charged a flat
291
+ // oneTimeFeePerStore (from the client's basepricing). Added AFTER the
292
+ // multi-month expansion so it's billed ONCE per invoice (not per month).
293
+ // It's a normal line item, so it lands in the taxable subtotal below.
294
+ try {
295
+ // Use the group's basepricing doc when group-wise pricing applies,
296
+ // else the brand-level doc.
297
+ const { query: oneTimeQuery } = await resolveBasePricingScope( group, getClient );
298
+ const oneTimeBp = await basepricingService.findOne(
299
+ oneTimeQuery,
300
+ { oneTimeFeePerStore: 1 },
301
+ );
302
+ const oneTimeFeePerStore = Number( oneTimeBp?.oneTimeFeePerStore ) || 0;
303
+ if ( oneTimeFeePerStore > 0 && Array.isArray( group.stores ) && group.stores.length ) {
304
+ const monthStart = new Date( baseDate.startOf( 'month' ).toISOString() );
305
+ const monthEnd = new Date( baseDate.endOf( 'month' ).toISOString() );
306
+ // Distinct group stores whose first file date is within the billing
307
+ // month. First file = edgefirstFileDate, fallback processfirstFileDate.
308
+ const newStoreAgg = await dailyPricingService.aggregate( [
309
+ { $match: { clientId: group.clientId } },
310
+ { $sort: { dateISO: -1 } },
311
+ { $limit: 1 },
312
+ { $project: { stores: { $filter: {
313
+ input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', group.stores ] },
314
+ } } } },
315
+ { $unwind: '$stores' },
316
+ { $addFields: { ff: {
317
+ $convert: {
318
+ input: { $ifNull: [ '$stores.edgefirstFileDate', '$stores.processfirstFileDate' ] },
319
+ to: 'date', onError: null, onNull: null,
320
+ },
321
+ } } },
322
+ { $match: { ff: { $ne: null, $gte: monthStart, $lte: monthEnd } } },
323
+ { $group: { _id: '$stores.storeId' } },
324
+ { $count: 'newStores' },
325
+ ] );
326
+ const newStoreCount = newStoreAgg?.[0]?.newStores || 0;
327
+ if ( newStoreCount > 0 ) {
328
+ products.push( {
329
+ productName: 'oneTimeFee',
330
+ period: 'fullmonth',
331
+ storeCount: newStoreCount,
332
+ amount: Math.round( newStoreCount * oneTimeFeePerStore * 100 ) / 100,
333
+ price: oneTimeFeePerStore,
334
+ description: `One-Time Fee - ${newStoreCount} stores`,
335
+ HsnNumber: '998314',
336
+ month: baseDate.format( 'MMM YYYY' ),
337
+ } );
338
+ }
339
+ }
340
+ } catch ( oneTimeErr ) {
341
+ logger.error( { error: oneTimeErr, function: 'createInvoice.oneTimeFee', clientId: group.clientId } );
342
+ }
343
+
289
344
  let amount = products.reduce( ( sum, product ) => sum + product.amount, 0 );
290
345
  let taxList = [];
291
346
  let totalAmount = 0;
@@ -474,7 +529,7 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
474
529
  // converting here would make the annexure unit price disagree with the actual
475
530
  // billed amount (e.g. a $45 negotiated price wrongly shown as $0.48).
476
531
 
477
- const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1, 'priceType': 1 } );
532
+ const annexClient = await clientService.findOne( { clientId: invoiceInfo.clientId }, { 'planDetails.product': 1, 'priceType': 1, 'billingGroupWisePricing': 1 } );
478
533
  const billingTypeMap = {};
479
534
  ( annexClient?.planDetails?.product || [] ).forEach( ( p ) => {
480
535
  billingTypeMap[p.productName] = p.billingType || 'perStore';
@@ -486,7 +541,18 @@ async function buildAnnexureRows( invoiceInfo, getgroup ) {
486
541
  // appear once per tier (e.g. the same store listed at $45 and again at $40).
487
542
  // Instead we pull the tiers here and assign each store a single tier price
488
543
  // below, in JS, by its 1-based index within the product.
489
- const pricingDoc = await basepricingService.findOne( { clientId: invoiceInfo.clientId }, { standard: 1, step: 1 } );
544
+ // Group-wise pricing: read the billing group's own doc when enabled and one
545
+ // exists; otherwise the brand-level doc.
546
+ let pricingDocQuery = { clientId: invoiceInfo.clientId, groupId: { $exists: false } };
547
+ if ( annexClient?.billingGroupWisePricing && getgroup?._id ) {
548
+ const grpDoc = await basepricingService.findOne(
549
+ { clientId: invoiceInfo.clientId, groupId: String( getgroup._id ) }, { _id: 1 },
550
+ );
551
+ if ( grpDoc ) {
552
+ pricingDocQuery = { clientId: invoiceInfo.clientId, groupId: String( getgroup._id ) };
553
+ }
554
+ }
555
+ const pricingDoc = await basepricingService.findOne( pricingDocQuery, { standard: 1, step: 1 } );
490
556
  const isStep = annexClient?.priceType === 'step';
491
557
  const tiersByProduct = {};
492
558
  ( ( isStep ? pricingDoc?.step : pricingDoc?.standard ) || [] ).forEach( ( p ) => {
@@ -1028,6 +1094,25 @@ function inWords( num ) {
1028
1094
  }
1029
1095
 
1030
1096
 
1097
+ // Resolve which basepricing doc applies to a billing group. When the client
1098
+ // has billingGroupWisePricing enabled AND a doc exists for this group, returns
1099
+ // that group's doc; otherwise falls back to the brand-level doc (groupId unset).
1100
+ // Returns { query, groupId } where query is the mongo filter for the chosen doc
1101
+ // and groupId is the group id to use in aggregation lookups (or null for brand).
1102
+ async function resolveBasePricingScope( group, getClient ) {
1103
+ const brandQuery = { clientId: group.clientId, groupId: { $exists: false } };
1104
+ if ( getClient?.billingGroupWisePricing && group?._id ) {
1105
+ const groupIdStr = String( group._id );
1106
+ const groupDoc = await basepricingService.findOne(
1107
+ { clientId: group.clientId, groupId: groupIdStr }, { _id: 1 },
1108
+ );
1109
+ if ( groupDoc ) {
1110
+ return { query: { clientId: group.clientId, groupId: groupIdStr }, groupId: groupIdStr };
1111
+ }
1112
+ }
1113
+ return { query: brandQuery, groupId: null };
1114
+ }
1115
+
1031
1116
  async function standardPrice( group, getClient, baseDate ) {
1032
1117
  console.log( '🚀 ~ standardPrice ~ baseDate:', baseDate.format( 'MMM YYYY' ) );
1033
1118
  const currentMonthDays = dayjs().daysInMonth();
@@ -1036,6 +1121,9 @@ async function standardPrice( group, getClient, baseDate ) {
1036
1121
  // Computed once so the aggregation pipelines can inline a $literal.
1037
1122
  const isFlatPricing = group.proRata === 'flat';
1038
1123
  console.log( '🚀 ~ standardPrice ~ isFlatPricing:', isFlatPricing );
1124
+ // Which basepricing doc applies (group-wise vs brand-level). pricingGroupId is
1125
+ // used in the $lookup pipeline so the join picks the right doc.
1126
+ const { groupId: pricingGroupId } = await resolveBasePricingScope( group, getClient );
1039
1127
  let billingTypeMap = {};
1040
1128
  if ( getClient?.planDetails?.product ) {
1041
1129
  getClient.planDetails.product.forEach( ( p ) => {
@@ -1172,7 +1260,14 @@ async function standardPrice( group, getClient, baseDate ) {
1172
1260
  {
1173
1261
  $match: {
1174
1262
  $expr: {
1175
- $eq: [ '$clientId', '$$clientId' ],
1263
+ $and: [
1264
+ { $eq: [ '$clientId', '$$clientId' ] },
1265
+ // Match the resolved doc: a specific group's doc when group-wise
1266
+ // pricing applies, else the brand-level doc (no groupId).
1267
+ pricingGroupId ?
1268
+ { $eq: [ '$groupId', pricingGroupId ] } :
1269
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1270
+ ],
1176
1271
  },
1177
1272
  },
1178
1273
  },
@@ -1367,7 +1462,12 @@ async function standardPrice( group, getClient, baseDate ) {
1367
1462
  from: 'basepricings',
1368
1463
  let: { clientId: group.clientId },
1369
1464
  pipeline: [
1370
- { $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
1465
+ { $match: { $expr: { $and: [
1466
+ { $eq: [ '$clientId', '$$clientId' ] },
1467
+ pricingGroupId ?
1468
+ { $eq: [ '$groupId', pricingGroupId ] } :
1469
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1470
+ ] } } },
1371
1471
  { $project: { standard: 1 } },
1372
1472
  ],
1373
1473
  as: 'basepricing',
@@ -1475,140 +1575,48 @@ async function stepPrice( group, getClient ) {
1475
1575
  // 'flat' => every store billed for full month.
1476
1576
  // 'prorate' => actual working days. See standardPrice for the same flag.
1477
1577
  const isFlatPricing = group.proRata === 'flat';
1578
+ // Which basepricing doc applies (group-wise vs brand-level).
1579
+ const { query: pricingDocQuery, groupId: pricingGroupId } = await resolveBasePricingScope( group, getClient );
1478
1580
  let billingTypeMap = {};
1479
1581
  if ( getClient?.planDetails?.product ) {
1480
1582
  getClient.planDetails.product.forEach( ( p ) => {
1481
1583
  billingTypeMap[p.productName] = p.billingType || 'perStore';
1482
1584
  } );
1483
1585
  }
1484
- let products = await dailyPricingService.aggregate( [
1485
- {
1486
- $match: {
1487
- clientId: group.clientId,
1488
- },
1489
- },
1490
- {
1491
- $sort: { dateISO: -1 },
1492
- },
1586
+ // PER-STORE rows (not bucketed) so step tiers can be assigned to individual
1587
+ // stores and each store charged tierPrice x its EXACT camera/zone count.
1588
+ let perStoreRows = await dailyPricingService.aggregate( [
1589
+ { $match: { clientId: group.clientId } },
1590
+ { $sort: { dateISO: -1 } },
1493
1591
  { $limit: 1 },
1494
- {
1495
- $project: {
1496
- stores: {
1497
- $filter: {
1498
- input: '$stores',
1499
- as: 'item',
1500
- cond: { $in: [ '$$item.storeId', group.stores ] },
1501
- },
1502
- },
1503
- },
1504
- },
1505
- {
1506
- $unwind: {
1507
- path: '$stores',
1508
- preserveNullAndEmptyArrays: false,
1509
- },
1510
- },
1511
- {
1512
- $unwind: {
1513
- path: '$stores.products',
1514
- preserveNullAndEmptyArrays: false,
1515
- },
1516
- },
1517
- {
1518
- $project: {
1519
- productName: '$stores.products.productName',
1520
- storeId: '$stores.storeId',
1521
- workingDays: '$stores.products.workingdays',
1522
- storeStatus: '$stores.status',
1523
- zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1524
- cameraCount: { $ifNull: [ '$stores.cameraCount', 0 ] },
1525
- // Pull per-store camera splits; same fix as standardPrice — without
1526
- // these the second $group sums missing fields and the perCamera
1527
- // branch in the downstream map silently falls back to perStore.
1528
- trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1529
- zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1530
- },
1531
- },
1532
- {
1533
- $match: {
1534
- workingDays: { $gt: 0 },
1535
- },
1536
- },
1537
- {
1538
- $project: {
1539
- productName: 1,
1540
- storeId: 1,
1541
- // Group-level flag baked into every doc; consumed by the next
1542
- // $project's $cond.
1543
- isFlatPricing: { $literal: isFlatPricing },
1544
- workingDays: 1,
1545
- storeStatus: 1,
1546
- zoneCount: 1,
1547
- cameraCount: 1,
1548
- trafficCameraCount: 1,
1549
- zoneCameraCount: 1,
1550
- },
1551
- },
1552
- {
1553
- $project: {
1554
- productName: 1,
1555
- storeId: 1,
1556
- // Flat => full month per store. Prorate => actual working days.
1557
- workingDays: {
1558
- $cond: { if: '$isFlatPricing', then: currentMonthDays, else: '$workingDays' },
1559
- },
1560
- storeStatus: 1,
1561
- zoneCount: 1,
1562
- cameraCount: 1,
1563
- trafficCameraCount: 1,
1564
- zoneCameraCount: 1,
1565
- },
1566
- }, {
1567
- $group: {
1568
- _id: {
1569
- productName: '$productName',
1570
- storeId: '$storeId',
1571
- },
1572
- workingdays: { $first: '$workingDays' },
1573
- zoneCount: { $first: '$zoneCount' },
1574
- cameraCount: { $first: '$cameraCount' },
1575
- trafficCameraCount: { $first: '$trafficCameraCount' },
1576
- zoneCameraCount: { $first: '$zoneCameraCount' },
1577
- },
1578
- },
1579
- {
1580
- $group: {
1581
- _id: {
1582
- productName: '$_id.productName',
1583
- workingdays: '$workingdays',
1584
- },
1585
- storeCount: { $sum: 1 },
1586
- totalZoneCount: { $sum: '$zoneCount' },
1587
- totalTrafficCameraCount: { $sum: '$trafficCameraCount' },
1588
- totalZoneCameraCount: { $sum: '$zoneCameraCount' },
1589
- },
1590
- },
1591
- {
1592
- $project: {
1593
- _id: 0,
1594
- productName: '$_id.productName',
1595
- workingdays: '$_id.workingdays',
1596
- storeCount: '$storeCount',
1597
- totalZoneCount: '$totalZoneCount',
1598
- totalTrafficCameraCount: '$totalTrafficCameraCount',
1599
- totalZoneCameraCount: '$totalZoneCameraCount',
1600
- },
1601
- },
1602
- {
1603
- // productName first so order is deterministic across views/PDF, then
1604
- // workingdays so step rows stay grouped consistently.
1605
- $sort: {
1606
- productName: 1,
1607
- workingdays: -1,
1608
- },
1609
- },
1610
-
1611
-
1592
+ { $project: { stores: { $filter: {
1593
+ input: '$stores', as: 'item', cond: { $in: [ '$$item.storeId', group.stores ] },
1594
+ } } } },
1595
+ { $unwind: { path: '$stores', preserveNullAndEmptyArrays: false } },
1596
+ { $unwind: { path: '$stores.products', preserveNullAndEmptyArrays: false } },
1597
+ { $project: {
1598
+ productName: '$stores.products.productName',
1599
+ storeId: '$stores.storeId',
1600
+ storeName: '$stores.storeName',
1601
+ workingDays: '$stores.products.workingdays',
1602
+ zoneCount: { $ifNull: [ '$stores.zoneCount', 0 ] },
1603
+ trafficCameraCount: { $ifNull: [ '$stores.trafficCameraCount', 0 ] },
1604
+ zoneCameraCount: { $ifNull: [ '$stores.zoneCameraCount', 0 ] },
1605
+ } },
1606
+ { $match: { workingDays: { $gt: 0 } } },
1607
+ // Flat pricing => bill every store for the full month.
1608
+ { $project: {
1609
+ _id: 0,
1610
+ productName: 1,
1611
+ storeId: 1,
1612
+ storeName: 1,
1613
+ workingDays: { $cond: { if: { $literal: isFlatPricing }, then: currentMonthDays, else: '$workingDays' } },
1614
+ zoneCount: 1,
1615
+ trafficCameraCount: 1,
1616
+ zoneCameraCount: 1,
1617
+ } },
1618
+ // Deterministic order so per-store tier assignment is stable run to run.
1619
+ { $sort: { productName: 1, storeId: 1 } },
1612
1620
  ] );
1613
1621
  // Build billingMethod map from group.products
1614
1622
  let billingMethodMap = {};
@@ -1670,7 +1678,12 @@ async function stepPrice( group, getClient ) {
1670
1678
  from: 'basepricings',
1671
1679
  let: { clientId: group.clientId },
1672
1680
  pipeline: [
1673
- { $match: { $expr: { $eq: [ '$clientId', '$$clientId' ] } } },
1681
+ { $match: { $expr: { $and: [
1682
+ { $eq: [ '$clientId', '$$clientId' ] },
1683
+ pricingGroupId ?
1684
+ { $eq: [ '$groupId', pricingGroupId ] } :
1685
+ { $not: [ { $ifNull: [ '$groupId', false ] } ] },
1686
+ ] } } },
1674
1687
  { $project: { step: 1 } },
1675
1688
  ],
1676
1689
  as: 'basepricing',
@@ -1679,7 +1692,7 @@ async function stepPrice( group, getClient ) {
1679
1692
  { $unwind: { path: '$basepricing', preserveNullAndEmptyArrays: true } },
1680
1693
  ] );
1681
1694
 
1682
- let stepPriceData = await basepricingService.findOne( { clientId: group.clientId } );
1695
+ let stepPriceData = await basepricingService.findOne( pricingDocQuery );
1683
1696
  let pricingRanges = stepPriceData?.step || [];
1684
1697
  let defaultPrice = pricingRanges.length > 0 ? pricingRanges[0].negotiatePrice : 0;
1685
1698
 
@@ -1721,168 +1734,100 @@ async function stepPrice( group, getClient ) {
1721
1734
  }
1722
1735
  }
1723
1736
 
1724
- // Filter out eachStore products from aggregated results
1725
- products = products.filter( ( p ) => !eachStoreProductNames.includes( p.productName ) );
1726
-
1727
- // Adjust storeCount based on billingType for tangoZone and tangoTraffic (overallStore only)
1728
- products = products.map( ( product ) => {
1729
- let productBillingType = billingTypeMap[product.productName] || 'perStore';
1730
- if ( product.productName === 'tangoZone' ) {
1731
- if ( productBillingType === 'perZone' && product.totalZoneCount > 0 ) {
1732
- product.storeCount = product.totalZoneCount;
1733
- } else if ( productBillingType === 'perCamera' && product.totalZoneCameraCount > 0 ) {
1734
- product.storeCount = product.totalZoneCameraCount;
1735
- }
1736
- } else if ( product.productName === 'tangoTraffic' ) {
1737
- if ( productBillingType === 'perCamera' && product.totalTrafficCameraCount > 0 ) {
1738
- product.storeCount = product.totalTrafficCameraCount;
1739
- }
1740
- }
1741
- delete product.totalZoneCount;
1742
- delete product.totalTrafficCameraCount;
1743
- delete product.totalZoneCameraCount;
1744
- return product;
1745
- } );
1737
+ // Drop eachStore products those are handled by the per-store branch above.
1738
+ perStoreRows = perStoreRows.filter( ( p ) => !eachStoreProductNames.includes( p.productName ) );
1746
1739
 
1747
- let stepPriceRecord = await basepricingService.findOne( { clientId: group.clientId } );
1748
- let data = products;
1749
- // Tiers must be ordered by range start for the cumulative step assignment in
1750
- // processArray to walk them low-to-high.
1751
- let pricing = ( stepPriceRecord.step || [] ).slice().sort( ( a, b ) => {
1740
+ const stepPriceRecord = await basepricingService.findOne( pricingDocQuery );
1741
+ // Tiers ordered by range start so per-store tier assignment walks low-to-high.
1742
+ const pricing = ( stepPriceRecord?.step || [] ).slice().sort( ( a, b ) => {
1752
1743
  const aStart = parseInt( String( a.storeRange || '0' ).split( '-' )[0], 10 ) || 0;
1753
1744
  const bStart = parseInt( String( b.storeRange || '0' ).split( '-' )[0], 10 ) || 0;
1754
1745
  return aStart - bStart;
1755
1746
  } );
1756
-
1757
- const applyPricing = ( data, pricing ) => {
1758
- let totalcount = 0;
1759
- return data.map( ( item ) => {
1760
- totalcount = totalcount + item.storeCount;
1761
-
1762
- console.log( '======>', item );
1763
- if ( item.workingdays === currentMonthDays ) {
1764
- item.period = 'fullMonth';
1765
- item.runningCost = item.storeCount * item.price;
1766
- } else {
1767
- item.period = 'proRate';
1768
- item.runningCost = ( item.storeCount * ( item.price / currentMonthDays ) * item.workingdays ).toFixed( 2 );
1747
+ // Tiers grouped per product (only one product set here, but keep it general).
1748
+ const tiersByProduct = {};
1749
+ for ( const t of pricing ) {
1750
+ if ( !tiersByProduct[t.productName] ) {
1751
+ tiersByProduct[t.productName] = [];
1752
+ }
1753
+ tiersByProduct[t.productName].push( t );
1754
+ }
1755
+ // Resolve the tier price for a store's 1-based position within its product.
1756
+ const tierPriceForPosition = ( productName, position ) => {
1757
+ const tiers = tiersByProduct[productName] || [];
1758
+ if ( !tiers.length ) {
1759
+ return 0;
1760
+ }
1761
+ for ( const t of tiers ) {
1762
+ const [ min, max ] = String( t.storeRange || '' ).split( '-' ).map( Number );
1763
+ if ( position >= min && position <= max ) {
1764
+ return Number( t.negotiatePrice ) || 0;
1769
1765
  }
1770
-
1771
- return {
1772
- ...item,
1773
- negotiatePrice: item.price,
1774
- perstorecost: ( ( item.price / currentMonthDays ) * item.workingdays ).toFixed( 2 ),
1775
- };
1776
- } );
1766
+ }
1767
+ // Beyond the last defined tier -> last tier's price.
1768
+ return Number( tiers[tiers.length - 1].negotiatePrice ) || 0;
1777
1769
  };
1778
- console.log( '---->', data, pricing );
1779
-
1780
-
1781
- function processArray( array1, array2 ) {
1782
- let updatedArray = [];
1783
1770
 
1784
- // Step tiers apply CUMULATIVELY across every working-days bucket of a
1785
- // product, not per bucket. Track how many stores of each product have
1786
- // already been priced so each bucket continues where the previous one left
1787
- // off. (Previously the tier counter reset for every bucket, so e.g. the 9
1788
- // prorated stores really stores 75-83, in the $40 tier — were each priced
1789
- // from tier 1 at $45, producing extra/wrong-priced line items.)
1790
- const assignedByProduct = {};
1791
-
1792
- for ( let item of array1 ) {
1793
- let remainingStores = item.storeCount;
1794
- let assigned = assignedByProduct[item.productName] || 0;
1795
-
1796
- for ( let range of array2 ) {
1797
- const [ min, max ] = range.storeRange.split( '-' ).map( Number );
1798
- if ( remainingStores <= 0 ) {
1799
- break;
1800
- }
1801
- // How many of this tier's slots are still free given everything already
1802
- // assigned to this product across previous buckets.
1803
- const tierCapacityLeft = max - Math.max( min - 1, assigned );
1804
- if ( tierCapacityLeft <= 0 ) {
1805
- continue;
1806
- }
1807
- const applicableStores = Math.min( remainingStores, tierCapacityLeft );
1808
- updatedArray.push( {
1809
- productName: item.productName,
1810
- workingdays: item.workingdays,
1811
- storeCount: applicableStores,
1812
- // Each tier is charged at its own range's negotiated price.
1813
- price: range.negotiatePrice,
1814
- } );
1815
- remainingStores -= applicableStores;
1816
- assigned += applicableStores;
1771
+ // EXACT per-store pricing: assign each store a 1-based position within its
1772
+ // product (tier boundary counted in STORES), then charge tierPrice x that
1773
+ // store's EXACT billable units cameras (perCamera) / zones (perZone) / 1
1774
+ // (perStore) prorated by working days. Group lines by product + period +
1775
+ // tier price so each tier collapses into a single line item.
1776
+ const productPosition = {};
1777
+ const grouped = {};
1778
+ for ( const s of perStoreRows ) {
1779
+ const billingType = billingTypeMap[s.productName] || 'perStore';
1780
+ let units = 1;
1781
+ if ( s.productName === 'tangoZone' ) {
1782
+ if ( billingType === 'perZone' && s.zoneCount > 0 ) {
1783
+ units = s.zoneCount;
1784
+ } else if ( billingType === 'perCamera' && s.zoneCameraCount > 0 ) {
1785
+ units = s.zoneCameraCount;
1817
1786
  }
1818
-
1819
- // Any stores beyond the last defined tier are charged at the last tier.
1820
- if ( remainingStores > 0 && array2.length ) {
1821
- updatedArray.push( {
1822
- productName: item.productName,
1823
- workingdays: item.workingdays,
1824
- storeCount: remainingStores,
1825
- price: array2[array2.length - 1].negotiatePrice,
1826
- } );
1827
- assigned += remainingStores;
1828
- remainingStores = 0;
1787
+ } else if ( s.productName === 'tangoTraffic' ) {
1788
+ if ( billingType === 'perCamera' && s.trafficCameraCount > 0 ) {
1789
+ units = s.trafficCameraCount;
1829
1790
  }
1830
-
1831
- assignedByProduct[item.productName] = assigned;
1832
1791
  }
1833
- return updatedArray;
1792
+ productPosition[s.productName] = ( productPosition[s.productName] || 0 ) + 1;
1793
+ const price = tierPriceForPosition( s.productName, productPosition[s.productName] );
1794
+ const fullMonth = s.workingDays >= currentMonthDays;
1795
+ const amount = fullMonth ?
1796
+ Math.round( units * price * 100 ) / 100 :
1797
+ Math.round( ( units * ( price / currentMonthDays ) * s.workingDays ) * 100 ) / 100;
1798
+ const period = fullMonth ? 'fullMonth' : 'proRate';
1799
+ const key = `${s.productName}_${period}_${price}`;
1800
+ if ( !grouped[key] ) {
1801
+ grouped[key] = { productName: s.productName, period, price, totalAmount: 0, unitCount: 0 };
1802
+ }
1803
+ grouped[key].totalAmount += amount;
1804
+ grouped[key].unitCount += units;
1834
1805
  }
1835
1806
 
1836
- let resultarray = processArray( data, pricing );
1837
- console.log( '++++++++++++', resultarray );
1838
- const result = applyPricing( resultarray, pricing );
1839
- console.log( '***********', result );
1840
- // Group by product + period + PRICE TIER so each step tier is its own line
1841
- // item, and all stores within a tier+period collapse into a single line:
1842
- // - full-month stores of a tier -> one line (sum cost, sum count)
1843
- // - prorated stores of a tier -> one averaged line (sum cost, sum count)
1844
- // Keying on price (not storeCount) is what merges the leftover prorated stores
1845
- // — which span several working-days buckets — into one $40 line instead of one
1846
- // line per working-days value.
1847
- const groupedData = result.reduce( ( acc, item ) => {
1848
- const { productName, period, runningCost } = item;
1849
- const key = `${productName}_${period}_${item.price}`;
1850
- if ( !acc[key] ) {
1851
- acc[key] = {
1852
- productName,
1853
- period,
1854
- totalRunningCost: 0,
1855
- count: 0,
1856
- };
1857
- }
1858
- acc[key].totalRunningCost += parseFloat( runningCost );
1859
- acc[key].count += item.storeCount;
1860
- return acc;
1861
- }, {} );
1862
- console.log( groupedData );
1863
- // Calculating average running cost
1864
- const finalresult = Object.values( groupedData ).map( ( grp ) => {
1807
+ const finalresult = Object.values( grouped ).map( ( grp ) => {
1865
1808
  let description = '';
1866
1809
  if ( grp.productName === 'tangoTraffic' ) {
1867
1810
  description = 'Customer Footfall Analytics';
1868
1811
  } else if ( grp.productName === 'tangoZone' ) {
1869
1812
  description = 'Product category/section analytics';
1870
- } else {
1871
- description = '';
1872
1813
  }
1814
+ const amount = Math.round( grp.totalAmount * 100 ) / 100;
1873
1815
  return {
1874
1816
  productName: grp.productName,
1875
1817
  period: grp.period,
1876
- storeCount: grp.count,
1818
+ // Quantity = billable units (cameras for perCamera, zones for perZone,
1819
+ // stores otherwise). For full-month lines price = tier price; for prorated
1820
+ // lines it's the effective per-unit cost (amount / units).
1821
+ storeCount: Math.round( grp.unitCount * 100 ) / 100,
1877
1822
  description: description,
1878
1823
  HsnNumber: '998314',
1879
- amount: grp.totalRunningCost,
1824
+ amount: amount,
1880
1825
  month: dayjs().format( 'MMM YYYY' ),
1881
- price: ( grp.totalRunningCost / grp.count ).toFixed( 2 ),
1826
+ price: grp.unitCount ? ( amount / grp.unitCount ).toFixed( 2 ) : '0.00',
1882
1827
  };
1883
1828
  } );
1884
1829
 
1885
- // Combine overallStore and eachStore products
1830
+ // Combine overallStore (per-store tiered) and eachStore products.
1886
1831
  return [ ...finalresult, ...eachStoreProducts ];
1887
1832
  }
1888
1833