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.
- package/package.json +2 -2
- package/src/controllers/bankTransaction.controller.js +617 -0
- package/src/controllers/brandsBilling.controller.js +443 -7
- package/src/controllers/estimate.controller.js +317 -0
- package/src/controllers/invoice.controller.js +172 -260
- package/src/controllers/paymentReminder.controller.js +81 -0
- package/src/controllers/paymentSubscription.controllers.js +55 -3
- package/src/dtos/validation.dtos.js +6 -0
- package/src/hbs/estimatePdf.hbs +125 -0
- package/src/hbs/invoicePdf.hbs +27 -0
- package/src/routes/billing.routes.js +5 -0
- package/src/routes/brandsBilling.routes.js +3 -1
- package/src/routes/invoice.routes.js +18 -0
- package/src/services/bankTransaction.service.js +21 -0
- package/src/services/estimate.service.js +25 -0
- package/src/services/paymentReminder.service.js +9 -0
|
@@ -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:
|
|
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:
|
|
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 & 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>
|
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -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
|
+
};
|