tango-app-api-payment-subscription 3.4.3 → 3.5.0
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 +8 -3
- 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 +387 -6
- package/src/controllers/invoice.controller.js +524 -30
- package/src/controllers/payment.controller.js +4 -3
- package/src/controllers/paymentSubscription.controllers.js +37 -14
- 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 +12 -2
- package/src/services/applicationDefault.service.js +13 -0
- package/src/utils/currency.js +14 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 3-Stage Invoice Approval Pipeline
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Invoices flow through four `status` values:
|
|
6
|
+
|
|
7
|
+
1. `pendingCsm` (default for new invoices)
|
|
8
|
+
2. `pendingFinance`
|
|
9
|
+
3. `pendingApproval`
|
|
10
|
+
4. `approved` (terminal — `paymentStatus` flow takes over)
|
|
11
|
+
|
|
12
|
+
## Transitions
|
|
13
|
+
|
|
14
|
+
| From | To | Endpoint | Permission |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| pendingCsm | pendingFinance | `POST /invoiceapi/approveInvoiceCsm` | `TangoAdmin.csmApproval.isEdit` |
|
|
17
|
+
| pendingFinance | pendingApproval | `POST /invoiceapi/approveInvoiceFinance` | `TangoAdmin.financeApproval.isEdit` |
|
|
18
|
+
| pendingApproval | approved | `POST /invoiceapi/approveInvoiceApproval` | `TangoAdmin.invoiceApproval.isEdit` |
|
|
19
|
+
|
|
20
|
+
All three accept the same body shape: `{ "invoiceId": "<24-char ObjectId>" }`.
|
|
21
|
+
|
|
22
|
+
The legacy `POST /invoiceDownload/:invoiceId` flow (with `sendInvoice: true, status: 'approved'`) is still supported BUT now requires the invoice to already be at `pendingApproval`. Earlier stages must use the dedicated advancement endpoints.
|
|
23
|
+
|
|
24
|
+
## Permission system setup
|
|
25
|
+
|
|
26
|
+
The `csmApproval` and `financeApproval` modules live under the `TangoAdmin` feature in the user-permission editor. Admin grants them per user via `/manage/users/tango` → Edit → TangoAdmin section → tick the matching Edit checkbox. No separate feature catalogue or seeding is required.
|
|
27
|
+
|
|
28
|
+
## Auditing
|
|
29
|
+
|
|
30
|
+
Every successful transition writes one OpenSearch entry with:
|
|
31
|
+
- `logSubType: 'invoiceStatusTransition'`
|
|
32
|
+
- `logType: 'invoice'`
|
|
33
|
+
- `changes: ["Invoice <invoiceNum> advanced from <fromStatus> to <toStatus> by <userEmail>"]`
|
|
34
|
+
|
|
35
|
+
## Migration
|
|
36
|
+
|
|
37
|
+
A one-shot script at `scripts/migrate-invoice-status-pipeline.js` migrates existing `pending` invoices to `pendingCsm`. Run it BEFORE deploying the schema enum change in `tango-api-schema`.
|
|
38
|
+
|
|
39
|
+
## Known limitations
|
|
40
|
+
|
|
41
|
+
- No rejection / step-back transitions (forward-only).
|
|
42
|
+
- No per-stage timestamps in the schema (use OpenSearch audit log).
|
|
43
|
+
- No email notifications on stage changes.
|
|
44
|
+
- Frontend has not been updated yet — invoices created after this change land at `pendingCsm` and the existing UI cannot advance them. UI feature tracked separately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-payment-subscription",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "paymentSubscription",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,22 +13,27 @@
|
|
|
13
13
|
"author": "praveenraj",
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"archiver": "^7.0.1",
|
|
16
17
|
"aws-sdk": "^2.1572.0",
|
|
17
18
|
"axios": "^1.6.8",
|
|
18
19
|
"dayjs": "^1.11.10",
|
|
19
20
|
"dotenv": "^16.4.5",
|
|
21
|
+
"exceljs": "^4.4.0",
|
|
20
22
|
"express": "^4.18.3",
|
|
21
23
|
"handlebars": "^4.7.8",
|
|
22
24
|
"html-pdf-node": "^1.0.8",
|
|
23
25
|
"joi-to-swagger": "^6.2.0",
|
|
24
26
|
"jsdom": "^24.0.0",
|
|
25
27
|
"mongodb": "^6.4.0",
|
|
28
|
+
"multer": "^1.4.5-lts.1",
|
|
26
29
|
"nodemon": "^3.1.0",
|
|
30
|
+
"puppeteer": "^24.41.0",
|
|
27
31
|
"swagger-ui-express": "^5.0.0",
|
|
28
|
-
"tango-api-schema": "^2.5.
|
|
32
|
+
"tango-api-schema": "^2.5.77",
|
|
29
33
|
"tango-app-api-middleware": "^3.6.18",
|
|
30
34
|
"winston": "^3.12.0",
|
|
31
|
-
"winston-daily-rotate-file": "^5.0.0"
|
|
35
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
36
|
+
"xlsx": "^0.18.5"
|
|
32
37
|
},
|
|
33
38
|
"devDependencies": {
|
|
34
39
|
"eslint": "^8.57.0",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// One-shot permission migration: ensures every tango user's `rolespermission`
|
|
2
|
+
// has the new `csmApproval` and `financeApproval` modules under TangoAdmin
|
|
3
|
+
// with `isEdit: true` granted. Idempotent — re-running it on already-migrated
|
|
4
|
+
// users is a no-op.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// MONGO_URI="mongodb://..." node scripts/grant-tango-approval-permissions.js
|
|
8
|
+
//
|
|
9
|
+
// Run this AFTER deploying the route guard change (Billing.* → TangoAdmin.*)
|
|
10
|
+
// so existing tango admins don't lose access to the approval pipeline.
|
|
11
|
+
|
|
12
|
+
import mongoose from 'mongoose';
|
|
13
|
+
import 'dotenv/config';
|
|
14
|
+
|
|
15
|
+
const FEATURE_NAME = 'TangoAdmin';
|
|
16
|
+
const NEW_MODULES = [ 'csmApproval', 'financeApproval' ];
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
const uri = process.env.MONGO_URI;
|
|
20
|
+
if ( !uri ) {
|
|
21
|
+
console.error( 'MONGO_URI env var is required' );
|
|
22
|
+
process.exit( 1 );
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await mongoose.connect( uri );
|
|
26
|
+
const User = mongoose.model(
|
|
27
|
+
'_migrateUser',
|
|
28
|
+
new mongoose.Schema( {}, { strict: false } ),
|
|
29
|
+
'users',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const tangoUsers = await User.find( { userType: 'tango' } ).lean();
|
|
33
|
+
console.log( `Found ${tangoUsers.length} tango user(s) to inspect` );
|
|
34
|
+
|
|
35
|
+
let modifiedCount = 0;
|
|
36
|
+
let alreadyOk = 0;
|
|
37
|
+
let needsTangoAdmin = 0;
|
|
38
|
+
|
|
39
|
+
for ( const user of tangoUsers ) {
|
|
40
|
+
const rolespermission = Array.isArray( user.rolespermission ) ? user.rolespermission : [];
|
|
41
|
+
let tangoAdmin = rolespermission.find( ( f ) => f && f.featureName === FEATURE_NAME );
|
|
42
|
+
let changed = false;
|
|
43
|
+
|
|
44
|
+
if ( !tangoAdmin ) {
|
|
45
|
+
// User has no TangoAdmin feature block at all — create one with just
|
|
46
|
+
// the two new modules. Other TangoAdmin modules (invoiceApproval etc.)
|
|
47
|
+
// stay unset so we don't accidentally grant unrelated access.
|
|
48
|
+
tangoAdmin = { featureName: FEATURE_NAME, modules: [] };
|
|
49
|
+
rolespermission.push( tangoAdmin );
|
|
50
|
+
needsTangoAdmin++;
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if ( !Array.isArray( tangoAdmin.modules ) ) {
|
|
55
|
+
tangoAdmin.modules = [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for ( const moduleName of NEW_MODULES ) {
|
|
59
|
+
if ( !tangoAdmin.modules.some( ( m ) => m && m.name === moduleName ) ) {
|
|
60
|
+
tangoAdmin.modules.push( { name: moduleName, isAdd: false, isEdit: true } );
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ( changed ) {
|
|
66
|
+
await User.updateOne( { _id: user._id }, { $set: { rolespermission } } );
|
|
67
|
+
modifiedCount++;
|
|
68
|
+
} else {
|
|
69
|
+
alreadyOk++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log( '' );
|
|
74
|
+
console.log( `Modified: ${modifiedCount} user(s)` );
|
|
75
|
+
console.log( `Already up to date: ${alreadyOk} user(s)` );
|
|
76
|
+
console.log( `New TangoAdmin blocks: ${needsTangoAdmin} (users that previously had no TangoAdmin feature)` );
|
|
77
|
+
|
|
78
|
+
await mongoose.disconnect();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
run().catch( ( err ) => {
|
|
82
|
+
console.error( err );
|
|
83
|
+
process.exit( 1 );
|
|
84
|
+
} );
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// One-shot migration: maps legacy invoice status 'pending' to the new
|
|
2
|
+
// pipeline starting stage 'pendingCsm'. Idempotent — safe to re-run.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// MONGO_URI="mongodb://..." node scripts/migrate-invoice-status-pipeline.js
|
|
6
|
+
//
|
|
7
|
+
// Sequencing: MUST run BEFORE deploying the schema enum change in
|
|
8
|
+
// tango-api-schema, otherwise in-flight writes that still set status:'pending'
|
|
9
|
+
// will throw ValidationError.
|
|
10
|
+
|
|
11
|
+
import mongoose from 'mongoose';
|
|
12
|
+
import 'dotenv/config';
|
|
13
|
+
|
|
14
|
+
const VALID_STATUSES = [ 'pendingCsm', 'pendingFinance', 'pendingApproval', 'approved' ];
|
|
15
|
+
|
|
16
|
+
async function run() {
|
|
17
|
+
const uri = process.env.MONGO_URI;
|
|
18
|
+
if ( !uri ) {
|
|
19
|
+
console.error( 'MONGO_URI env var is required' );
|
|
20
|
+
process.exit( 1 );
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await mongoose.connect( uri );
|
|
24
|
+
// Use strict:false so we don't depend on a particular schema package version
|
|
25
|
+
// for this script — it operates on raw documents.
|
|
26
|
+
const Invoice = mongoose.model(
|
|
27
|
+
'_migrateInvoice',
|
|
28
|
+
new mongoose.Schema( {}, { strict: false } ),
|
|
29
|
+
'invoices',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// 1. Map legacy 'pending' rows to 'pendingCsm'.
|
|
33
|
+
const pendingResult = await Invoice.updateMany(
|
|
34
|
+
{ status: 'pending' },
|
|
35
|
+
{ $set: { status: 'pendingCsm' } },
|
|
36
|
+
);
|
|
37
|
+
console.log( `Migrated ${pendingResult.modifiedCount} invoice(s) from 'pending' → 'pendingCsm'` );
|
|
38
|
+
|
|
39
|
+
// 2. Surface any rows whose status isn't in the new enum (for manual review).
|
|
40
|
+
const stray = await Invoice.find(
|
|
41
|
+
{ status: { $nin: VALID_STATUSES } },
|
|
42
|
+
{ _id: 1, invoice: 1, status: 1 },
|
|
43
|
+
).lean();
|
|
44
|
+
|
|
45
|
+
if ( stray.length > 0 ) {
|
|
46
|
+
console.warn( `\n${stray.length} invoice(s) have a status outside the new enum:` );
|
|
47
|
+
for ( const s of stray ) {
|
|
48
|
+
console.warn( ` _id=${s._id} invoice=${s.invoice} status=${JSON.stringify( s.status )}` );
|
|
49
|
+
}
|
|
50
|
+
console.warn( '\nReview these manually before deploying the schema change.' );
|
|
51
|
+
} else {
|
|
52
|
+
console.log( '\nAll invoice rows are in a known state. Safe to publish schema.' );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await mongoose.disconnect();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
run().catch( ( err ) => {
|
|
59
|
+
console.error( err );
|
|
60
|
+
process.exit( 1 );
|
|
61
|
+
} );
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { logger, insertOpenSearchData } from 'tango-app-api-middleware';
|
|
2
|
+
import { findOneApplicationDefault, upsertApplicationDefault } from '../services/applicationDefault.service.js';
|
|
3
|
+
|
|
4
|
+
const INVOICE_HEADS_QUERY = { type: 'invoice', subType: 'heads' };
|
|
5
|
+
|
|
6
|
+
export async function getInvoiceHeads( req, res ) {
|
|
7
|
+
try {
|
|
8
|
+
const doc = await findOneApplicationDefault( INVOICE_HEADS_QUERY );
|
|
9
|
+
const data = ( doc && Array.isArray( doc.data ) ) ? doc.data : [];
|
|
10
|
+
return res.sendSuccess( { data } );
|
|
11
|
+
} catch ( error ) {
|
|
12
|
+
logger.error( { error: error, function: 'getInvoiceHeads' } );
|
|
13
|
+
return res.sendError( error, 500 );
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function updateInvoiceHeads( req, res ) {
|
|
18
|
+
try {
|
|
19
|
+
if ( req.user?.role !== 'superadmin' ) {
|
|
20
|
+
return res.sendError( 'Only superadmin can update invoice settings', 403 );
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = req.body.data;
|
|
24
|
+
const payload = {
|
|
25
|
+
type: 'invoice',
|
|
26
|
+
subType: 'heads',
|
|
27
|
+
data,
|
|
28
|
+
active: true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await upsertApplicationDefault( INVOICE_HEADS_QUERY, payload );
|
|
32
|
+
|
|
33
|
+
insertOpenSearchData( JSON.parse( process.env.OPENSEARCH ).activityLog, {
|
|
34
|
+
userName: req.user?.userName,
|
|
35
|
+
email: req.user?.email,
|
|
36
|
+
clientId: '',
|
|
37
|
+
logSubType: 'invoiceSettingsUpdate',
|
|
38
|
+
logType: 'applicationDefault',
|
|
39
|
+
date: new Date(),
|
|
40
|
+
changes: [ `Invoice heads updated by ${req.user?.email}: ${JSON.stringify( data )}` ],
|
|
41
|
+
eventType: '',
|
|
42
|
+
timestamp: new Date(),
|
|
43
|
+
showTo: [ 'tango' ],
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
return res.sendSuccess( { data } );
|
|
47
|
+
} catch ( error ) {
|
|
48
|
+
logger.error( { error: error, function: 'updateInvoiceHeads' } );
|
|
49
|
+
return res.sendError( error, 500 );
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -9,6 +9,7 @@ import { leadGet } from '../services/lead.service.js';
|
|
|
9
9
|
import { findOneClient } from '../services/clientPayment.services.js';
|
|
10
10
|
import * as billingService from '../services/billing.service.js';
|
|
11
11
|
import * as storeService from '../services/store.service.js';
|
|
12
|
+
import { symbolFor } from '../utils/currency.js';
|
|
12
13
|
export const subscribedStoreList = async ( req, res ) => {
|
|
13
14
|
try {
|
|
14
15
|
const matchStage = {
|
|
@@ -819,7 +820,7 @@ export const onetimePayment = async ( req, res ) => {
|
|
|
819
820
|
|
|
820
821
|
await invoiceService.invoiceUpdateOne( { invoice: invoice.invoice }, { $set: { 'tax': invoice._doc.tax, 'amount': invoice._doc.amount, 'totalAmount': invoice._doc.totalAmount } } );
|
|
821
822
|
let getgroup = await billingService.findOne( { _id: invoice.groupId } );
|
|
822
|
-
let currency= getgroup.currency
|
|
823
|
+
let currency = symbolFor( getgroup.currency );
|
|
823
824
|
let logObj = {
|
|
824
825
|
userName: req.user?.userName,
|
|
825
826
|
email: req.user?.email,
|