tango-app-api-payment-subscription 3.5.4 → 3.5.6

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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  /* eslint-disable new-cap */
3
- import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById, sendTeamsNotification } from 'tango-app-api-middleware';
3
+ import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById } from 'tango-app-api-middleware';
4
4
  import * as paymentService from '../services/clientPayment.services.js';
5
5
  import * as basePriceService from '../services/basePrice.service.js';
6
6
  import * as storeService from '../services/store.service.js';
@@ -102,6 +102,15 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
102
102
  let storeCount = await storeService.count( { clientId: clientInfo[0].clientId, status: 'active' } );
103
103
  let tangoProductsList = await basePricingService.findOne( { clientId: { $exists: false } }, { basePricing: 1 } );
104
104
  let tangoProducts = tangoProductsList.basePricing.map( ( item ) => item.productName );
105
+ // Client-specific negotiated pricing — used to show the agreed per-store
106
+ // price for subscribed (live) products in the upgrade-plan popup.
107
+ let clientPricing = await basePricingService.findOne( { clientId: clientInfo[0].clientId }, { standard: 1, step: 1 } );
108
+ let negotiateByProduct = {};
109
+ ( clientPricing?.standard || clientPricing?.step || [] ).forEach( ( p ) => {
110
+ if ( p && p.productName != null && p.negotiatePrice != null ) {
111
+ negotiateByProduct[p.productName] = p.negotiatePrice;
112
+ }
113
+ } );
105
114
  let activeProducts = clientInfo[0].planDetails.product;
106
115
  let liveProducts = [];
107
116
  let trialProducts = [];
@@ -160,6 +169,10 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
160
169
  if ( price ) {
161
170
  element.price = price.basePrice;
162
171
  }
172
+ // Negotiated (agreed) per-store price for subscribed products.
173
+ if ( negotiateByProduct[element.productName] != null ) {
174
+ element.negotiatePrice = negotiateByProduct[element.productName];
175
+ }
163
176
  let getProductCount = productDetails.find( ( item ) => item.product == element.productName );
164
177
  element.storeCount = getProductCount?.count || 0;
165
178
  element.aliseProductName = convertTitleCase( element.productName );
@@ -2546,17 +2559,55 @@ async function updatePricing( req, res, update ) {
2546
2559
  if ( clientDetails ) {
2547
2560
  let products = clientDetails.planDetails.product.map( ( item ) => item.productName );
2548
2561
  let subscriptionProduct = clientDetails.planDetails.product.filter( ( item ) => item.status == 'live' );
2562
+ // Negotiated prices coming from the Subscribe popup (or any caller) take
2563
+ // precedence; otherwise we keep whatever was already saved for this
2564
+ // client and only fall back to the global base defaults for brand-new
2565
+ // products. This stops updatePricing from clobbering a manually agreed
2566
+ // price every time the product list changes.
2567
+ let pricingOverride = {};
2568
+ if ( Array.isArray( req.body.pricing ) ) {
2569
+ req.body.pricing.forEach( ( p ) => {
2570
+ if ( p && p.productName ) {
2571
+ pricingOverride[p.productName] = p;
2572
+ }
2573
+ } );
2574
+ }
2575
+ logger.info?.( { function: 'updatePricing', clientId: req.body.clientId, override: pricingOverride } );
2576
+ let existingStandard = {};
2577
+ let existingStep = {};
2578
+ ( getPriceInfo?.standard || [] ).forEach( ( s ) => {
2579
+ existingStandard[s.productName] = s;
2580
+ } );
2581
+ ( getPriceInfo?.step || [] ).forEach( ( s ) => {
2582
+ existingStep[s.productName] = s;
2583
+ } );
2549
2584
  let standardList = [];
2550
2585
  let stepList = [];
2551
2586
  products.forEach( ( product ) => {
2552
2587
  let baseDetails = baseProduct.basePricing.find( ( item ) => item.productName == product );
2588
+ // A product missing from the global base pricing would otherwise crash
2589
+ // the whole pricing save (and silently drop the negotiated override,
2590
+ // since this runs un-awaited in a .then()). Fall back to safe zeros.
2591
+ if ( !baseDetails ) {
2592
+ baseDetails = { basePrice: 0, discoutPercentage: 0 };
2593
+ }
2553
2594
  let discountPrice = ( baseDetails.basePrice * baseDetails.discoutPercentage ) / 100;
2595
+ let defaultNegotiate = Number( baseDetails.basePrice - discountPrice );
2596
+ // Precedence: explicit override from the request > previously saved
2597
+ // client price > computed default from the global base pricing.
2598
+ let override = pricingOverride[product];
2599
+ let prevStandard = existingStandard[product];
2600
+ let prevStep = existingStep[product];
2601
+ let negotiateStandard = override && override.negotiatePrice != null ? Number( override.negotiatePrice ) :
2602
+ ( prevStandard && prevStandard.negotiatePrice != null ? Number( prevStandard.negotiatePrice ) : defaultNegotiate );
2603
+ let negotiateStep = override && override.negotiatePrice != null ? Number( override.negotiatePrice ) :
2604
+ ( prevStep && prevStep.negotiatePrice != null ? Number( prevStep.negotiatePrice ) : defaultNegotiate );
2554
2605
  standardList.push(
2555
2606
  {
2556
2607
  productName: product,
2557
2608
  discountPercentage: baseDetails.discoutPercentage,
2558
2609
  basePrice: baseDetails.basePrice,
2559
- negotiatePrice: Number( baseDetails.basePrice - discountPrice ),
2610
+ negotiatePrice: negotiateStandard,
2560
2611
  },
2561
2612
  );
2562
2613
  stepList.push(
@@ -2564,8 +2615,8 @@ async function updatePricing( req, res, update ) {
2564
2615
  productName: product,
2565
2616
  discountPercentage: baseDetails.discoutPercentage,
2566
2617
  basePrice: baseDetails.basePrice,
2567
- negotiatePrice: Number( baseDetails.basePrice - discountPrice ),
2568
- storeRange: '1-100',
2618
+ negotiatePrice: negotiateStep,
2619
+ storeRange: ( prevStep && prevStep.storeRange ) || '1-100',
2569
2620
  },
2570
2621
  );
2571
2622
  } );
@@ -2574,6 +2625,7 @@ async function updatePricing( req, res, update ) {
2574
2625
  step: stepList,
2575
2626
  clientId: req.body.clientId,
2576
2627
  };
2628
+ console.log( '🚀 ~ updatePricing ~ data:', data );
2577
2629
  if ( !getPriceInfo ) {
2578
2630
  await basePricingService.create( data );
2579
2631
  } else {
@@ -3332,7 +3384,7 @@ export const dailyPricingInsert = async ( req, res ) => {
3332
3384
  ];
3333
3385
  let dailyData = await dailyPriceService.aggregate( query );
3334
3386
  let cameraDetails = await cameraService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], isActivated: true, isUp: true }, { streamName: 1, productModule: 1 } );
3335
- console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3387
+ // console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3336
3388
 
3337
3389
  let trafficCameraCount = cameraDetails.filter( ( cam ) =>
3338
3390
  ( cam.productModule || [] ).some( ( mod ) =>
@@ -3351,14 +3403,16 @@ export const dailyPricingInsert = async ( req, res ) => {
3351
3403
  mod.productName === 'tangoZone' && mod.checked === true,
3352
3404
  ),
3353
3405
  ).map( ( cam ) => cam.streamName );
3354
- let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: zoneCameraStreamNames } }, { tagName: 1 } );
3406
+ let allcameraname = cameraDetails?.map( ( cam ) => cam?.streamName );
3407
+ console.log( '🚀 ~ dailyPricingInsert ~ zoneCameraStreamNames:', zoneCameraStreamNames );
3408
+ let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: allcameraname } }, { tagName: 1 } );
3355
3409
  let zoneCount = taggingDetails.length;
3356
3410
  let zoneName = taggingDetails.map( ( item ) => item.tagName );
3357
3411
  let firstDate = dayjs( getStore[storeIndex]?.edge?.firstFileDate ).format( 'YYYY-MM-DD' );
3358
3412
  let workingdays;
3359
3413
  let workingdaystrax;
3360
3414
  const givenDate = dayjs( requestData.date );
3361
- console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', trafficCameraCount, zoneCameraCount, zoneCount );
3415
+ console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', getStore[storeIndex].storeId, trafficCameraCount, zoneCameraCount, zoneCount );
3362
3416
  const isFirstDayOfMonth = givenDate.isSame( dayjs().startOf( 'month' ), 'day' );
3363
3417
  if ( getStore[storeIndex]?.edge.firstFile ) {
3364
3418
  if ( firstDate < requestData.date && getStore[storeIndex]?.status == 'active' &&
@@ -3471,13 +3525,24 @@ export const dailyPricingInsert = async ( req, res ) => {
3471
3525
  }
3472
3526
  // console.log( clientIndex, requestClient.length-1 );
3473
3527
  if ( clientIndex == requestClient.length - 1 ) {
3474
- let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3528
+ // let teamsAlertUrls = process.env.teamsAlertURL ? JSON.parse( process.env.teamsAlertURL ) : '';
3475
3529
  let totalcount = await dailyPriceService.find( { dateString: dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' ) } );
3476
3530
  let teamsMsg = `${totalcount.length} clients data is inserted on ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3477
3531
  console.log( teamsMsg );
3478
- if ( teamsAlertUrls.invoiceAlert ) {
3479
- sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3532
+ // if ( teamsAlertUrls.invoiceAlert ) {
3533
+ // sendTeamsNotification( teamsAlertUrls.invoiceAlert, teamsMsg );
3534
+ // }
3535
+
3536
+ const SES = JSON.parse( process.env.SES );
3537
+ let fromEmail = SES.accountsEmail;
3538
+ console.log( process.env.invoiceAlert );
3539
+ let invoiceEmails = JSON.parse( process.env.invoiceAlert );
3540
+ let mailSubject = `Daily Invoice Alert ${dayjs( requestData.date, 'YYYY-MM-DD' ).format( 'YYYY-MM-DD' )}`;
3541
+ if ( invoiceEmails ) {
3542
+ const result = await sendEmailWithSES( invoiceEmails?.email, mailSubject, teamsMsg, '', fromEmail, [] );
3543
+ console.log( '🚀 ~ dailyPricingInsert ~ result:', result );
3480
3544
  }
3545
+
3481
3546
  return res.sendSuccess( 'Price Details Inserted Successfully' );
3482
3547
  }
3483
3548
  }
@@ -75,6 +75,12 @@ export const validateSubscibeSchema = joi.object( {
75
75
  product: joi.array().required(),
76
76
  clientId: joi.string().required(),
77
77
  stores: joi.array().optional(),
78
+ // Negotiated price overrides from the Subscribe popup — applied in
79
+ // updatePricing so a manually-agreed price isn't reset to base defaults.
80
+ pricing: joi.array().items( joi.object( {
81
+ productName: joi.string().required(),
82
+ negotiatePrice: joi.number().min( 0 ).required(),
83
+ } ).unknown( true ) ).optional(),
78
84
  } );
79
85
 
80
86
  export const validateSubscibeParams = {
@@ -314,6 +320,7 @@ export const createBillingGroupBody = joi.object(
314
320
  proRata: joi.string().optional().allow( '' ),
315
321
  paymentCategory: joi.string().optional().allow( '' ),
316
322
  currency: joi.string().optional().allow( '' ),
323
+ taxCalculationType: joi.string().valid( 'domestic', 'international' ).optional(),
317
324
  isInstallationOneTime: joi.boolean().optional(),
318
325
  installationFee: joi.number().optional(),
319
326
  paymentCycle: joi.string().optional().allow( '' ),
@@ -352,6 +359,7 @@ export const updateBillingGroupBody = joi.object(
352
359
  proRata: joi.string().optional().allow( '' ),
353
360
  paymentCategory: joi.string().optional().allow( '' ),
354
361
  currency: joi.string().optional().allow( '' ),
362
+ taxCalculationType: joi.string().valid( 'domestic', 'international' ).optional(),
355
363
  isInstallationOneTime: joi.boolean().optional(),
356
364
  installationFee: joi.number().optional(),
357
365
  paymentCycle: joi.string().optional().allow( '' ),
@@ -0,0 +1,125 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <style>
6
+ * { box-sizing: border-box; }
7
+ body { font-family: Arial, Helvetica, sans-serif; color: #1f2937; font-size: 12px; margin: 0; }
8
+ .wrap { padding: 6px 4px; }
9
+ .head { display: flex; justify-content: space-between; align-items: flex-start; }
10
+ .brand { display: flex; align-items: center; gap: 10px; }
11
+ .brand img { height: 40px; }
12
+ .doc-title { font-size: 26px; font-weight: 800; color: #101828; letter-spacing: .5px; }
13
+ .doc-sub { color: #667085; font-size: 12px; margin-top: 2px; }
14
+ .pill { display: inline-block; padding: 3px 11px; border-radius: 999px; font-size: 11px; font-weight: 700; }
15
+ .pill-draft { background: #f1f3f6; color: #667085; }
16
+ .pill-sent { background: #e8f3fe; color: #1573c4; }
17
+ .pill-accepted { background: #e7f7ee; color: #138a52; }
18
+ .pill-declined, .pill-expired { background: #fdecec; color: #d64545; }
19
+ .row { display: flex; justify-content: space-between; margin-top: 22px; }
20
+ .label { font-size: 10px; letter-spacing: .05em; text-transform: uppercase; color: #98a2b3; font-weight: 700; }
21
+ .client-name { font-size: 15px; font-weight: 700; color: #101828; margin-top: 4px; }
22
+ .meta td { padding: 2px 0; }
23
+ .meta .m-label { color: #98a2b3; padding-right: 14px; }
24
+ .meta .m-val { font-weight: 700; color: #344054; text-align: right; }
25
+ .total-box { text-align: right; }
26
+ .total-box .t-label { color: #667085; font-size: 12px; font-weight: 600; }
27
+ .total-box .t-amt { font-size: 24px; font-weight: 800; color: #101828; }
28
+ table.items { width: 100%; border-collapse: collapse; margin-top: 26px; }
29
+ table.items thead th { background: #f4f6f9; color: #667085; font-size: 10px; text-transform: uppercase;
30
+ letter-spacing: .04em; text-align: left; padding: 9px 10px; border-bottom: 1px solid #e4e6ea; }
31
+ table.items tbody td { padding: 11px 10px; border-bottom: 1px solid #eceef1; vertical-align: top; }
32
+ .prod-name { font-weight: 700; color: #101828; }
33
+ .prod-desc { color: #98a2b3; font-size: 11px; margin-top: 2px; }
34
+ .ta-r { text-align: right; }
35
+ .ta-c { text-align: center; }
36
+ .totals { width: 46%; margin-left: auto; margin-top: 16px; }
37
+ .totals td { padding: 6px 0; }
38
+ .totals .tl { color: #667085; }
39
+ .totals .tv { text-align: right; font-weight: 700; color: #101828; }
40
+ .totals .grand td { border-top: 1px solid #e4e6ea; padding-top: 10px; font-size: 15px; font-weight: 800; }
41
+ .note { margin-top: 24px; background: #f7f8fa; border: 1px solid #eceef1; border-radius: 8px;
42
+ padding: 10px 14px; color: #475467; }
43
+ .foot { margin-top: 26px; color: #98a2b3; font-size: 11px; border-top: 1px solid #eceef1; padding-top: 10px; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div class="wrap">
48
+ <div class="head">
49
+ <div class="brand">
50
+ <img src="{{logo}}" alt="">
51
+ <div>
52
+ <div style="font-weight:800;font-size:16px;color:#101828">Tango Eye</div>
53
+ <div class="doc-sub">Tango IT Solutions India Pvt Ltd</div>
54
+ </div>
55
+ </div>
56
+ <div style="text-align:right">
57
+ <div class="doc-title">ESTIMATE</div>
58
+ <div class="doc-sub">{{estimate}}</div>
59
+ <div style="margin-top:6px"><span class="pill pill-{{status}}">{{statusLabel}}</span></div>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="row">
64
+ <div style="max-width:55%">
65
+ <div class="label">Estimate For</div>
66
+ <div class="client-name">{{companyName}}</div>
67
+ {{#if companyAddress}}<div style="margin-top:3px">{{companyAddress}}</div>{{/if}}
68
+ {{#if GSTNumber}}<div style="margin-top:6px"><strong>GSTIN {{GSTNumber}}</strong></div>{{/if}}
69
+ {{#if PlaceOfSupply}}<div>Place Of Supply: {{PlaceOfSupply}}</div>{{/if}}
70
+ {{#if groupName}}<div style="margin-top:6px">Billing Group: {{groupName}}</div>{{/if}}
71
+ </div>
72
+ <div class="total-box">
73
+ <div class="t-label">Estimate Total</div>
74
+ <div class="t-amt">{{currencyType}} {{totalAmount}}</div>
75
+ <table class="meta" style="margin-top:12px;margin-left:auto">
76
+ <tr><td class="m-label">Period</td><td class="m-val">{{period}}</td></tr>
77
+ <tr><td class="m-label">Generated</td><td class="m-val">{{createdDate}}</td></tr>
78
+ <tr><td class="m-label">Valid Till</td><td class="m-val">{{validTill}}</td></tr>
79
+ </table>
80
+ </div>
81
+ </div>
82
+
83
+ <table class="items">
84
+ <thead>
85
+ <tr>
86
+ <th style="width:36px">#</th>
87
+ <th>Product &amp; Description</th>
88
+ <th style="width:90px">HSN/SAC</th>
89
+ <th class="ta-c" style="width:80px">Stores</th>
90
+ <th class="ta-r" style="width:110px">Rate</th>
91
+ <th class="ta-r" style="width:120px">Amount</th>
92
+ </tr>
93
+ </thead>
94
+ <tbody>
95
+ {{#each products}}
96
+ <tr>
97
+ <td>{{index}}</td>
98
+ <td>
99
+ <div class="prod-name">{{productName}}</div>
100
+ {{#if description}}<div class="prod-desc">{{description}}</div>{{/if}}
101
+ </td>
102
+ <td>{{hsn}}</td>
103
+ <td class="ta-c"><strong>{{storeCount}}</strong></td>
104
+ <td class="ta-r">{{../currencyType}} {{price}}</td>
105
+ <td class="ta-r">{{../currencyType}} {{amount}}</td>
106
+ </tr>
107
+ {{/each}}
108
+ </tbody>
109
+ </table>
110
+
111
+ <table class="totals">
112
+ <tr><td class="tl">Sub Total</td><td class="tv">{{currencyType}} {{amount}}</td></tr>
113
+ {{#each tax}}
114
+ <tr><td class="tl">{{type}} ({{value}}%)</td><td class="tv">{{../currencyType}} {{taxAmount}}</td></tr>
115
+ {{/each}}
116
+ <tr class="grand"><td class="tl">Total Amount</td><td class="tv">{{currencyType}} {{totalAmount}}</td></tr>
117
+ </table>
118
+
119
+ {{#if notes}}<div class="note">{{notes}}</div>{{/if}}
120
+
121
+ <div class="foot">This is an estimate, not a tax invoice. Valid until {{validTill}}. Prices are subject to the
122
+ terms agreed in the final subscription.</div>
123
+ </div>
124
+ </body>
125
+ </html>
@@ -1730,6 +1730,30 @@
1730
1730
  {{/each}}
1731
1731
 
1732
1732
  </div>
1733
+ <div class="column" style="max-width: 90px;">
1734
+ <div class="table-header-cell" style="padding: 13px 30px 12px 22px; background-color:#D0D5DD;">
1735
+ <div class="table-header">
1736
+ <div class="text6">Qty</div>
1737
+ </div>
1738
+ </div>
1739
+ {{#each annuxureData}}
1740
+ <div class="table-cell" style="height: 70px !important;">
1741
+ <div class="text7">{{units}}</div>
1742
+ </div>
1743
+ {{/each}}
1744
+ </div>
1745
+ <div class="column" style="max-width: 150px;">
1746
+ <div class="table-header-cell" style="padding: 13px 30px 12px 22px; background-color:#D0D5DD;">
1747
+ <div class="table-header">
1748
+ <div class="text6">Unit Price</div>
1749
+ </div>
1750
+ </div>
1751
+ {{#each annuxureData}}
1752
+ <div class="table-cell" style="height: 70px !important;">
1753
+ <div class="text7">{{../currencyType}} {{standardPrice}}<br>{{unitBasis}}</div>
1754
+ </div>
1755
+ {{/each}}
1756
+ </div>
1733
1757
  <div class="column">
1734
1758
  <div class="table-header-cell" style="padding: 13px 60px 12px 22px; background-color:#D0D5DD;">
1735
1759
  <div class="table-header">
@@ -1744,6 +1768,9 @@
1744
1768
  </div>
1745
1769
  </div>
1746
1770
  </div>
1771
+ <div style="display:flex; justify-content:flex-end; margin-top:14px; padding-right:10px;">
1772
+ <div class="text6" style="font-weight:700;">Total: {{currencyType}} {{annuxureTotal}}</div>
1773
+ </div>
1747
1774
  {{/eq}}
1748
1775
  </div>
1749
1776
  </div>
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  export const billingRouter = express.Router();
3
3
  import { accessVerification, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
4
- import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup } from '../controllers/billing.controllers.js';
4
+ import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup, gstinLookup } from '../controllers/billing.controllers.js';
5
5
  import { billingGroupSchema, clientProductsValid, createBillingGroupsSchema, deleteBillingGroupsSchema, getBillingGroupsSchema, getInvoiceSchema, leadProductsValid, onetimeFeeValid, subscribedStoreListSchema, updateBillingGroupsSchema } from '../dtos/validation.dtos.js';
6
6
 
7
7
 
@@ -27,3 +27,4 @@ billingRouter.get( '/getLeadProducts/:email', isAllowedSessionHandler, validate(
27
27
  billingRouter.get( '/getClientProducts/:id', isAllowedSessionHandler, validate( clientProductsValid ), getClientProducts );
28
28
 
29
29
 
30
+ billingRouter.get( '/gst-lookup/:gstin', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), gstinLookup );
@@ -1,6 +1,6 @@
1
1
 
2
2
  import express from 'express';
3
- import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups } from '../controllers/brandsBilling.controller.js';
3
+ import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary } from '../controllers/brandsBilling.controller.js';
4
4
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
5
5
 
6
6
  export const brandsBillingRouter = express.Router();
@@ -14,3 +14,4 @@ brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandle
14
14
  brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getClientBillingInfo );
15
15
  brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
16
16
  brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpdateBillingGroups );
17
+ brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
@@ -1,7 +1,9 @@
1
1
  import express from 'express';
2
- import { createInvoice, invoiceDownload, invoiceDownloadBulk, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, invoiceAnnexure, updateInvoice, getClientBasePricing, deleteInvoice, approveInvoiceCsm, approveInvoiceFinance, approveInvoiceApproval } from '../controllers/invoice.controller.js';
2
+ import { createInvoice, invoiceDownload, invoiceDownloadBulk, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, invoiceAnnexure, updateInvoice, getClientBasePricing, deleteInvoice, approveInvoiceCsm, approveInvoiceFinance, approveInvoiceApproval, recordPayment } from '../controllers/invoice.controller.js';
3
3
  import { isAllowedSessionHandler, accessVerification, validate } from 'tango-app-api-middleware';
4
4
  import { getInvoiceHeads, updateInvoiceHeads } from '../controllers/applicationDefault.controllers.js';
5
+ import { uploadBankStatement, bankTransactionList, resolveOptions, resolveBankTransaction } from '../controllers/bankTransaction.controller.js';
6
+ import { estimateList, createEstimate, getEstimate, estimateStatusUpdate, deleteEstimate, downloadEstimate } from '../controllers/estimate.controller.js';
5
7
  import { updateInvoiceHeadsSchema } from '../dtos/validation.dtos.js';
6
8
  export const invoiceRouter = express.Router();
7
9
 
@@ -29,6 +31,7 @@ invoiceRouter.post( '/pendingInvoices', isAllowedSessionHandler, pendingInvoices
29
31
  invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), applyDiscount );
30
32
  invoiceRouter.post( '/migrateInvoice', migrateInvoice );
31
33
  invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
34
+ invoiceRouter.post( '/recordPayment', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), recordPayment );
32
35
  invoiceRouter.post( '/checkPaymentStatus', checkPaymentStatus );
33
36
  invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
34
37
  invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
@@ -42,3 +45,19 @@ invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, superadm
42
45
 
43
46
  invoiceRouter.get( '/getInvoiceHeads', isAllowedSessionHandler, getInvoiceHeads );
44
47
  invoiceRouter.post( '/updateInvoiceHeads', isAllowedSessionHandler, validate( updateInvoiceHeadsSchema ), updateInvoiceHeads );
48
+
49
+ // Bank-statement reconciliation (billing "Transactions" tab). Upload mutates
50
+ // the banktransaction collection so it needs edit rights; list is read-only.
51
+ invoiceRouter.post( '/bankStatement/upload', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), uploadBankStatement );
52
+ invoiceRouter.post( '/bankStatement/list', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), bankTransactionList );
53
+ invoiceRouter.get( '/bankStatement/resolveOptions', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), resolveOptions );
54
+ invoiceRouter.post( '/bankStatement/resolve', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), resolveBankTransaction );
55
+
56
+ // Estimates (quotations) — per-brand quotation documents with their own
57
+ // lifecycle. List/get are read; create/status/delete need edit rights.
58
+ invoiceRouter.post( '/estimate/list', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), estimateList );
59
+ invoiceRouter.post( '/estimate/create', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), createEstimate );
60
+ invoiceRouter.post( '/estimate/download/:estimateId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), downloadEstimate );
61
+ invoiceRouter.get( '/estimate/:estimateId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getEstimate );
62
+ invoiceRouter.post( '/estimate/statusUpdate', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), estimateStatusUpdate );
63
+ invoiceRouter.delete( '/estimate/:estimateId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteEstimate );
@@ -0,0 +1,21 @@
1
+ import model from 'tango-api-schema';
2
+
3
+ // Bank-statement transactions (uploaded from the billing Transactions tab).
4
+ // Lives in its own `banktransaction` collection — the legacy `transaction`
5
+ // collection holds payment/wallet entries and must not be mixed with these.
6
+
7
+ export const insertMany = async ( data ) => {
8
+ return await model.bankTransactionModel.insertMany( data );
9
+ };
10
+
11
+ export const aggregate = async ( query = [] ) => {
12
+ return await model.bankTransactionModel.aggregate( query );
13
+ };
14
+
15
+ export const find = async ( query = {}, projection = {} ) => {
16
+ return await model.bankTransactionModel.find( query, projection );
17
+ };
18
+
19
+ export const updateOne = async ( filter, update ) => {
20
+ return await model.bankTransactionModel.updateOne( filter, update );
21
+ };
@@ -0,0 +1,25 @@
1
+ import model from 'tango-api-schema';
2
+
3
+ export const aggregate = async ( query = [] ) => {
4
+ return await model.estimateModel.aggregate( query );
5
+ };
6
+
7
+ export const find = async ( query = {}, projection = {} ) => {
8
+ return await model.estimateModel.find( query, projection );
9
+ };
10
+
11
+ export const findOne = async ( query = {}, projection = {} ) => {
12
+ return await model.estimateModel.findOne( query, projection );
13
+ };
14
+
15
+ export const create = async ( data ) => {
16
+ return await model.estimateModel.create( data );
17
+ };
18
+
19
+ export const updateOne = async ( filter, update ) => {
20
+ return await model.estimateModel.updateOne( filter, update );
21
+ };
22
+
23
+ export const count = async ( query = {} ) => {
24
+ return await model.estimateModel.countDocuments( query );
25
+ };
@@ -9,6 +9,11 @@ export const findOneAccount = ( query = {}, record = {} ) => {
9
9
  export const updateOneAccount = async ( query ={}, record={} ) => {
10
10
  return await model.paymentAccountModel.updateOne( query, { $set: record } );
11
11
  };
12
+
13
+ export const createAccount = async ( record ) => {
14
+ return await model.paymentAccountModel.create( record );
15
+ };
16
+
12
17
  export const aggregate = ( query = [] ) => {
13
18
  return model.paymentAccountModel.aggregate( query );
14
19
  };