tango-app-api-payment-subscription 3.4.4 → 3.5.1
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/docs/invoice-approval-pipeline.md +44 -0
- package/package.json +7 -2
- package/scripts/grant-tango-approval-permissions.js +84 -0
- package/scripts/migrate-invoice-status-pipeline.js +61 -0
- package/src/controllers/applicationDefault.controllers.js +51 -0
- package/src/controllers/billing.controllers.js +2 -1
- package/src/controllers/brandsBilling.controller.js +562 -4
- package/src/controllers/invoice.controller.js +469 -9
- package/src/controllers/payment.controller.js +4 -3
- package/src/controllers/paymentSubscription.controllers.js +23 -9
- package/src/dtos/validation.dtos.js +55 -0
- package/src/hbs/invoicePdf.hbs +8 -0
- package/src/routes/brandsBilling.routes.js +17 -1
- package/src/routes/invoice.routes.js +29 -7
- package/src/services/applicationDefault.service.js +13 -0
- package/src/utils/currency.js +14 -0
|
@@ -371,6 +371,31 @@ export const updateBillingGroupsSchema = {
|
|
|
371
371
|
body: updateBillingGroupBody,
|
|
372
372
|
};
|
|
373
373
|
|
|
374
|
+
export const bulkUpdateBillingGroupRowSchema = joi.object( {
|
|
375
|
+
groupName: joi.string().required(),
|
|
376
|
+
groupTag: joi.string().required(),
|
|
377
|
+
registeredCompanyName: joi.string().required(),
|
|
378
|
+
gst: joi.string().allow( '' ).optional(),
|
|
379
|
+
addressLineOne: joi.string().allow( '' ).optional(),
|
|
380
|
+
addressLineTwo: joi.string().allow( '' ).optional(),
|
|
381
|
+
city: joi.string().allow( '' ).optional(),
|
|
382
|
+
state: joi.string().allow( '' ).optional(),
|
|
383
|
+
country: joi.string().allow( '' ).optional(),
|
|
384
|
+
pinCode: joi.string().allow( '' ).optional(),
|
|
385
|
+
placeOfSupply: joi.string().allow( '' ).optional(),
|
|
386
|
+
po: joi.string().allow( '' ).optional(),
|
|
387
|
+
proRata: joi.string().allow( '' ).optional(),
|
|
388
|
+
paymentCategory: joi.string().allow( '' ).optional(),
|
|
389
|
+
currency: joi.string().allow( '' ).optional(),
|
|
390
|
+
paymentCycle: joi.string().allow( '' ).optional(),
|
|
391
|
+
paymentTerm: joi.number().integer().min( 0 ).max( 365 ).optional(),
|
|
392
|
+
installationFee: joi.number().min( 0 ).optional(),
|
|
393
|
+
isInstallationOneTime: joi.boolean().optional(),
|
|
394
|
+
attachAnnexure: joi.boolean().optional(),
|
|
395
|
+
advanceInvoice: joi.boolean().optional(),
|
|
396
|
+
storeId: joi.string().allow( '' ).optional(),
|
|
397
|
+
} );
|
|
398
|
+
|
|
374
399
|
export const deleteBillingGroupQuery = {
|
|
375
400
|
_id: joi.string().required(),
|
|
376
401
|
};
|
|
@@ -468,3 +493,33 @@ export const clientProductsParam = joi.object().keys( {
|
|
|
468
493
|
export const clientProductsValid = {
|
|
469
494
|
params: clientProductsParam,
|
|
470
495
|
};
|
|
496
|
+
|
|
497
|
+
const INVOICE_STATUS_VALUES = [ 'pendingCsm', 'pendingFinance', 'pendingApproval', 'approved' ];
|
|
498
|
+
|
|
499
|
+
export const invoiceStatusEnum = INVOICE_STATUS_VALUES;
|
|
500
|
+
|
|
501
|
+
export const approveInvoiceTransitionBody = joi.object( {
|
|
502
|
+
invoiceId: joi.string().length( 24 ).hex().required(),
|
|
503
|
+
} );
|
|
504
|
+
|
|
505
|
+
export const approveInvoiceTransitionSchema = {
|
|
506
|
+
body: approveInvoiceTransitionBody,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const updateInvoiceHeadsBody = joi.object( {
|
|
510
|
+
data: joi.array().items(
|
|
511
|
+
joi.object( {
|
|
512
|
+
role: joi.string().valid( 'csm', 'finance' ).required(),
|
|
513
|
+
// email may be a single string (csm) or an array of strings (finance).
|
|
514
|
+
// Empty string / empty array are allowed for un-setting.
|
|
515
|
+
email: joi.alternatives().try(
|
|
516
|
+
joi.string().email().allow( '' ),
|
|
517
|
+
joi.array().items( joi.string().email() ),
|
|
518
|
+
).required(),
|
|
519
|
+
} ),
|
|
520
|
+
).min( 1 ).required(),
|
|
521
|
+
} );
|
|
522
|
+
|
|
523
|
+
export const updateInvoiceHeadsSchema = {
|
|
524
|
+
body: updateInvoiceHeadsBody,
|
|
525
|
+
};
|
package/src/hbs/invoicePdf.hbs
CHANGED
|
@@ -1612,6 +1612,14 @@
|
|
|
1612
1612
|
{{#eq billingCurrency 'dollar'}}
|
|
1613
1613
|
US Dollar
|
|
1614
1614
|
{{/eq}}
|
|
1615
|
+
|
|
1616
|
+
{{#eq billingCurrency 'singaporedollar'}}
|
|
1617
|
+
Singapore Dollar
|
|
1618
|
+
{{/eq}}
|
|
1619
|
+
|
|
1620
|
+
{{#eq billingCurrency 'euro'}}
|
|
1621
|
+
Euro
|
|
1622
|
+
{{/eq}}
|
|
1615
1623
|
{{amountwords}} Only
|
|
1616
1624
|
</div>
|
|
1617
1625
|
</div>
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
import express from 'express';
|
|
3
|
-
import
|
|
3
|
+
import multer from 'multer';
|
|
4
|
+
import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups } from '../controllers/brandsBilling.controller.js';
|
|
4
5
|
import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
|
|
6
|
+
|
|
7
|
+
const bulkUpload = multer( {
|
|
8
|
+
storage: multer.memoryStorage(),
|
|
9
|
+
limits: { fileSize: 5 * 1024 * 1024 },
|
|
10
|
+
fileFilter: ( _req, file, cb ) => {
|
|
11
|
+
const ok = [
|
|
12
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
13
|
+
'application/vnd.ms-excel',
|
|
14
|
+
'application/octet-stream',
|
|
15
|
+
].includes( file.mimetype );
|
|
16
|
+
cb( ok ? null : new Error( 'Invalid file format — use XLSX' ), ok );
|
|
17
|
+
},
|
|
18
|
+
} );
|
|
5
19
|
export const brandsBillingRouter = express.Router();
|
|
6
20
|
|
|
7
21
|
brandsBillingRouter.post( '/brandsBillingList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), brandsBillingList );
|
|
@@ -11,3 +25,5 @@ brandsBillingRouter.post( '/brandBillingGroups', isAllowedSessionHandler, access
|
|
|
11
25
|
brandsBillingRouter.put( '/updateDailyPricingWorkingDays', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingWorkingDays );
|
|
12
26
|
brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingStoreField );
|
|
13
27
|
brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getClientBillingInfo );
|
|
28
|
+
brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
|
|
29
|
+
brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpload.single( 'file' ), bulkUpdateBillingGroups );
|
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { createInvoice, invoiceDownload, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, updateInvoice, getClientBasePricing, deleteInvoice } from '../controllers/invoice.controller.js';
|
|
3
|
-
import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
|
|
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';
|
|
3
|
+
import { isAllowedSessionHandler, accessVerification, validate } from 'tango-app-api-middleware';
|
|
4
|
+
import { getInvoiceHeads, updateInvoiceHeads } from '../controllers/applicationDefault.controllers.js';
|
|
5
|
+
import { updateInvoiceHeadsSchema } from '../dtos/validation.dtos.js';
|
|
4
6
|
export const invoiceRouter = express.Router();
|
|
5
7
|
|
|
8
|
+
// Superadmin bypass: if the authenticated user has role 'superadmin', skip the
|
|
9
|
+
// granular access check that follows. Place between isAllowedSessionHandler and
|
|
10
|
+
// accessVerification so req.user is populated.
|
|
11
|
+
function superadminBypass( accessConfig ) {
|
|
12
|
+
const verify = accessVerification( accessConfig );
|
|
13
|
+
return function( req, res, next ) {
|
|
14
|
+
if ( req.user && req.user.role === 'superadmin' ) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
return verify( req, res, next );
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
|
|
7
22
|
invoiceRouter.post( '/createInvoice', isAllowedSessionHandler, createInvoice );
|
|
8
23
|
invoiceRouter.post( '/regerateInvoice', isAllowedSessionHandler, createInvoice );
|
|
24
|
+
invoiceRouter.post( '/invoiceDownload/bulk', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceDownloadBulk );
|
|
9
25
|
invoiceRouter.post( '/invoiceDownload/:invoiceId', isAllowedSessionHandler, invoiceDownload );
|
|
10
26
|
invoiceRouter.post( '/clientInvoiceList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), clientInvoiceList );
|
|
11
27
|
invoiceRouter.post( '/creditTransactionlist', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), creditTransactionlist );
|
|
12
28
|
invoiceRouter.post( '/pendingInvoices', isAllowedSessionHandler, pendingInvoices );
|
|
13
|
-
invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler,
|
|
29
|
+
invoiceRouter.post( '/applyDiscount', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), applyDiscount );
|
|
14
30
|
invoiceRouter.post( '/migrateInvoice', migrateInvoice );
|
|
15
|
-
invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler,
|
|
31
|
+
invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
|
|
16
32
|
invoiceRouter.post( '/checkPaymentStatus', checkPaymentStatus );
|
|
17
|
-
invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler,
|
|
18
|
-
invoiceRouter.
|
|
33
|
+
invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
|
|
34
|
+
invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
|
|
35
|
+
invoiceRouter.put( '/updateInvoice', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateInvoice );
|
|
19
36
|
invoiceRouter.get( '/getClientBasePricing/:clientId', isAllowedSessionHandler, getClientBasePricing );
|
|
20
|
-
invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler,
|
|
37
|
+
invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteInvoice );
|
|
21
38
|
|
|
39
|
+
invoiceRouter.post( '/approveInvoiceCsm', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'csmApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceCsm );
|
|
40
|
+
invoiceRouter.post( '/approveInvoiceFinance', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'financeApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceFinance );
|
|
41
|
+
invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, superadminBypass( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceApproval );
|
|
22
42
|
|
|
43
|
+
invoiceRouter.get( '/getInvoiceHeads', isAllowedSessionHandler, getInvoiceHeads );
|
|
44
|
+
invoiceRouter.post( '/updateInvoiceHeads', isAllowedSessionHandler, validate( updateInvoiceHeadsSchema ), updateInvoiceHeads );
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import model from 'tango-api-schema';
|
|
2
|
+
|
|
3
|
+
export async function findOneApplicationDefault( query, projection ) {
|
|
4
|
+
return model.applicationDefaultModel.findOne( query, projection );
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function upsertApplicationDefault( query, payload ) {
|
|
8
|
+
return model.applicationDefaultModel.findOneAndUpdate(
|
|
9
|
+
query,
|
|
10
|
+
{ $set: payload },
|
|
11
|
+
{ new: true, upsert: true },
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised currency symbol map for invoices, transactions, and PDFs.
|
|
3
|
+
* Keys must match the values stored in billing.currency / transaction.currency
|
|
4
|
+
* enums (see tango-api-schema). Unknown keys fall back to '$' to preserve the
|
|
5
|
+
* pre-existing two-currency-ternary behaviour.
|
|
6
|
+
*/
|
|
7
|
+
export const CURRENCY_SYMBOLS = {
|
|
8
|
+
inr: '₹',
|
|
9
|
+
dollar: '$',
|
|
10
|
+
singaporedollar: 'S$',
|
|
11
|
+
euro: '€',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const symbolFor = ( currency ) => CURRENCY_SYMBOLS[currency] ?? '$';
|