tango-app-api-payment-subscription 3.5.5 → 3.5.7

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.
@@ -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 {
@@ -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 = {
@@ -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,6 +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 { getPaymentReminder, savePaymentReminder } from '../controllers/paymentReminder.controller.js';
4
5
  import { createBillingGroup, deleteBillingGroup, getAllBillingGroups, getBillingGroups, getClientProducts, getInvoices, getLeadProducts, onetimePayment, subscribedStoreList, updateBillingGroup, gstinLookup } from '../controllers/billing.controllers.js';
5
6
  import { billingGroupSchema, clientProductsValid, createBillingGroupsSchema, deleteBillingGroupsSchema, getBillingGroupsSchema, getInvoiceSchema, leadProductsValid, onetimeFeeValid, subscribedStoreListSchema, updateBillingGroupsSchema } from '../dtos/validation.dtos.js';
6
7
 
@@ -28,3 +29,7 @@ billingRouter.get( '/getClientProducts/:id', isAllowedSessionHandler, validate(
28
29
 
29
30
 
30
31
  billingRouter.get( '/gst-lookup/:gstin', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), gstinLookup );
32
+
33
+ // Payment reminder config (Billing Settings page), one document per brand.
34
+ billingRouter.get( '/payment-reminder/:clientId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), getPaymentReminder );
35
+ billingRouter.post( '/payment-reminder', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Billing', permissions: [ 'isAdd' ] } ] } ), savePaymentReminder );
@@ -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,5 @@ 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.post( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
18
+ brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
@@ -2,6 +2,8 @@ import express from 'express';
2
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
 
@@ -43,3 +45,19 @@ invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, superadm
43
45
 
44
46
  invoiceRouter.get( '/getInvoiceHeads', isAllowedSessionHandler, getInvoiceHeads );
45
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
+ };
@@ -0,0 +1,9 @@
1
+ import model from 'tango-api-schema';
2
+
3
+ export const findOne = async ( query = {}, projection = {} ) => {
4
+ return await model.paymentReminderModel.findOne( query, projection );
5
+ };
6
+
7
+ export const upsert = async ( filter, update ) => {
8
+ return await model.paymentReminderModel.updateOne( filter, { $set: update }, { upsert: true } );
9
+ };