non-gdty flow modified and docker compose updated
This commit is contained in:
parent
aaa249c341
commit
55804671e5
@ -1 +1 @@
|
||||
import{a as s}from"./index-bnlsiWxc.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CX5oLBI_.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
import{a as s}from"./index-BCZm9H2Q.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CX5oLBI_.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
64
build/assets/index-BCZm9H2Q.js
Normal file
64
build/assets/index-BCZm9H2Q.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-DjE6S9VF.css
Normal file
1
build/assets/index-DjE6S9VF.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
||||
<!-- Preload essential fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<script type="module" crossorigin src="/assets/index-bnlsiWxc.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BCZm9H2Q.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||
@ -21,7 +21,7 @@
|
||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tioiXSPh.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DjE6S9VF.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -1,39 +1,8 @@
|
||||
# =============================================================================
|
||||
# RE Workflow - Full Stack Docker Compose
|
||||
# Includes: Application + Database + Monitoring Stack
|
||||
# =============================================================================
|
||||
# Usage:
|
||||
# docker-compose -f docker-compose.full.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
# docker-compose.full.yml
|
||||
# Synced with streamlined infrastructure
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# ===========================================================================
|
||||
# APPLICATION SERVICES
|
||||
# ===========================================================================
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: re_workflow_db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-laxman}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-Admin@123}
|
||||
POSTGRES_DB: ${DB_NAME:-re_workflow_db}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./database/schema:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-laxman}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: re_workflow_redis
|
||||
@ -69,73 +38,6 @@ services:
|
||||
networks:
|
||||
- re_workflow_network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: re_workflow_backend
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${DB_USER:-laxman}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-Admin@123}
|
||||
DB_NAME: ${DB_NAME:-re_workflow_db}
|
||||
REDIS_URL: redis://redis:6379
|
||||
CLAMD_HOST: clamav
|
||||
CLAMD_PORT: 3310
|
||||
PORT: 5000
|
||||
# Loki for logging
|
||||
LOKI_HOST: http://loki:3100
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./uploads:/app/uploads
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:5000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})\""]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# ===========================================================================
|
||||
# MONITORING SERVICES
|
||||
# ===========================================================================
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.47.2
|
||||
container_name: re_prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./monitoring/prometheus/alert.rules.yml:/etc/prometheus/alert.rules.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--storage.tsdb.retention.time=15d'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/-/healthy"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
loki:
|
||||
image: grafana/loki:2.9.2
|
||||
container_name: re_loki
|
||||
@ -177,15 +79,12 @@ services:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=REWorkflow@2024
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_FEATURE_TOGGLES_ENABLE=publicDashboards
|
||||
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource,grafana-piechart-panel
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./monitoring/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
|
||||
- ./monitoring/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
|
||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
depends_on:
|
||||
- prometheus
|
||||
- loki
|
||||
networks:
|
||||
- re_workflow_network
|
||||
@ -196,56 +95,13 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.6.1
|
||||
container_name: re_node_exporter
|
||||
ports:
|
||||
- "9100:9100"
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
redis_data:
|
||||
clamav_data:
|
||||
loki_data:
|
||||
promtail_data:
|
||||
grafana_data:
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.26.0
|
||||
container_name: re_alertmanager
|
||||
ports:
|
||||
- "9093:9093"
|
||||
volumes:
|
||||
- ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||
- alertmanager_data:/alertmanager
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
|
||||
# ===========================================================================
|
||||
# NETWORKS
|
||||
# ===========================================================================
|
||||
networks:
|
||||
re_workflow_network:
|
||||
driver: bridge
|
||||
name: re_workflow_network
|
||||
|
||||
# ===========================================================================
|
||||
# VOLUMES
|
||||
# ===========================================================================
|
||||
volumes:
|
||||
postgres_data:
|
||||
name: re_postgres_data
|
||||
redis_data:
|
||||
name: re_redis_data
|
||||
clamav_data:
|
||||
name: re_clamav_data
|
||||
prometheus_data:
|
||||
name: re_prometheus_data
|
||||
loki_data:
|
||||
name: re_loki_data
|
||||
promtail_data:
|
||||
name: re_promtail_data
|
||||
grafana_data:
|
||||
name: re_grafana_data
|
||||
alertmanager_data:
|
||||
name: re_alertmanager_data
|
||||
|
||||
|
||||
@ -1,28 +1,8 @@
|
||||
# docker-compose.yml
|
||||
# Streamlined infrastructure for local development
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: re_workflow_db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-laxman}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-Admin@123}
|
||||
POSTGRES_DB: ${DB_NAME:-re_workflow_db}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./database/schema:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-laxman}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: re_workflow_redis
|
||||
@ -58,46 +38,69 @@ services:
|
||||
networks:
|
||||
- re_workflow_network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: re_workflow_backend
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${DB_USER:-laxman}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-Admin@123}
|
||||
DB_NAME: ${DB_NAME:-re_workflow_db}
|
||||
REDIS_URL: redis://redis:6379
|
||||
CLAMD_HOST: clamav
|
||||
CLAMD_PORT: 3310
|
||||
PORT: 5000
|
||||
loki:
|
||||
image: grafana/loki:2.9.2
|
||||
container_name: re_loki
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
- "3100:3100"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./uploads:/app/uploads
|
||||
- ./monitoring/loki/loki-config.yml:/etc/loki/local-config.yaml:ro
|
||||
- loki_data:/loki
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:5000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})\""]
|
||||
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:2.9.2
|
||||
container_name: re_promtail
|
||||
volumes:
|
||||
- ./monitoring/promtail/promtail-config.yml:/etc/promtail/config.yml:ro
|
||||
- ./logs:/var/log/app:ro
|
||||
- promtail_data:/tmp/promtail
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
depends_on:
|
||||
- loki
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:10.2.2
|
||||
container_name: re_grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=REWorkflow@2024
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./monitoring/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
|
||||
- ./monitoring/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
|
||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
depends_on:
|
||||
- loki
|
||||
networks:
|
||||
- re_workflow_network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
clamav_data:
|
||||
loki_data:
|
||||
promtail_data:
|
||||
grafana_data:
|
||||
|
||||
networks:
|
||||
re_workflow_network:
|
||||
|
||||
@ -791,31 +791,28 @@ export class DealerClaimController {
|
||||
return ResponseHandler.error(res, 'Invoice record not found', 404);
|
||||
}
|
||||
|
||||
// Automatically regenerate PDF to ensure latest template/data is used (useful during testing/fixes)
|
||||
// Generate PDF on the fly
|
||||
try {
|
||||
const { pdfService } = await import('../services/pdf.service');
|
||||
await pdfService.generateInvoicePdf(requestId);
|
||||
// Re-fetch invoice to get the new filePath
|
||||
invoice = await ClaimInvoice.findOne({ where: { requestId } });
|
||||
} catch (pdfError) {
|
||||
logger.error(`[DealerClaimController] Failed to auto-regenerate PDF:`, pdfError);
|
||||
// Continue with existing file if regeneration fails
|
||||
}
|
||||
const pdfBuffer = await pdfService.generateInvoicePdf(requestId);
|
||||
|
||||
if (!invoice || !invoice.filePath) {
|
||||
return ResponseHandler.error(res, 'Invoice PDF not found', 404);
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'storage', 'invoices', invoice.filePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return ResponseHandler.error(res, 'Invoice file not found on server', 404);
|
||||
}
|
||||
const requestNumber = workflow.requestNumber || 'invoice';
|
||||
const fileName = `Invoice_${requestNumber}.pdf`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${invoice.filePath}"`);
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.setHeader('Content-Length', pdfBuffer.length);
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
fileStream.pipe(res);
|
||||
// Convert Buffer to stream
|
||||
const { Readable } = await import('stream');
|
||||
const stream = new Readable();
|
||||
stream.push(pdfBuffer);
|
||||
stream.push(null);
|
||||
stream.pipe(res);
|
||||
} catch (pdfError) {
|
||||
logger.error(`[DealerClaimController] Failed to generate PDF:`, pdfError);
|
||||
return ResponseHandler.error(res, 'Failed to generate invoice PDF', 500);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[DealerClaimController] Error downloading invoice PDF:', error);
|
||||
@ -1020,9 +1017,11 @@ export class DealerClaimController {
|
||||
logger.info(`[DealerClaimController] Found ${items.length} items to export for request ${requestNumber}`);
|
||||
|
||||
let sapRefNo = '';
|
||||
let taxationType = 'GST';
|
||||
if (claimDetails?.activityType) {
|
||||
const activityType = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
sapRefNo = activityType?.sapRefNo || '';
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
sapRefNo = activity?.sapRefNo || '';
|
||||
taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||
}
|
||||
|
||||
// Construct CSV
|
||||
@ -1040,7 +1039,10 @@ export class DealerClaimController {
|
||||
];
|
||||
|
||||
const rows = items.map(item => {
|
||||
const trnsUniqNo = item.transactionCode || '';
|
||||
const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||
|
||||
// For Non-GST, we hide HSN (often stored in transactionCode) and GST details
|
||||
const trnsUniqNo = isNonGst ? '' : (item.transactionCode || '');
|
||||
const claimNumber = requestNumber;
|
||||
const invNumber = invoice?.invoiceNumber || '';
|
||||
const dealerCode = claimDetails?.dealerCode || '';
|
||||
@ -1048,8 +1050,10 @@ export class DealerClaimController {
|
||||
const claimDocTyp = sapRefNo;
|
||||
const claimDate = invoice?.createdAt ? new Date(invoice.createdAt).toISOString().split('T')[0] : '';
|
||||
const claimAmt = item.assAmt;
|
||||
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
|
||||
const gstPercentag = item.gstRt;
|
||||
|
||||
// Zero out tax for Non-GST
|
||||
const totalTax = isNonGst ? 0 : (Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0));
|
||||
const gstPercentag = isNonGst ? 0 : (item.gstRt || 0);
|
||||
|
||||
return [
|
||||
trnsUniqNo,
|
||||
|
||||
@ -168,6 +168,11 @@ async function performScan(
|
||||
console.log(`[MalwareScan] ⚠️ File validation warnings for "${file.originalname}": ${validation.warnings.join('; ')}`);
|
||||
}
|
||||
|
||||
// 🟢 SANITIZATION: Update originalname with the sanitized version
|
||||
if (validation.sanitizedFilename) {
|
||||
file.originalname = validation.sanitizedFilename;
|
||||
}
|
||||
|
||||
// Check if we have a buffer (memory storage) or path (disk storage)
|
||||
if (file.buffer) {
|
||||
tempPath = writeTempFile(file.buffer, file.originalname);
|
||||
@ -326,6 +331,11 @@ async function performMultiScan(
|
||||
return;
|
||||
}
|
||||
|
||||
// 🟢 SANITIZATION: Update originalname with the sanitized version for each file in the array
|
||||
if (validation.sanitizedFilename) {
|
||||
file.originalname = validation.sanitizedFilename;
|
||||
}
|
||||
|
||||
// Write to temp if memory storage
|
||||
if (file.buffer) {
|
||||
tempPath = writeTempFile(file.buffer, file.originalname);
|
||||
|
||||
@ -1084,15 +1084,44 @@ export class DealerClaimService {
|
||||
if (claimDetails) {
|
||||
serializedClaimDetails = (claimDetails as any).toJSON ? (claimDetails as any).toJSON() : claimDetails;
|
||||
|
||||
// Add default GST rate from ActivityType
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
if (activity) {
|
||||
serializedClaimDetails.defaultGstRate = Number(activity.gstRate) || 18;
|
||||
serializedClaimDetails.taxationType = activity.taxationType || null;
|
||||
} else {
|
||||
serializedClaimDetails.defaultGstRate = 18; // Fallback
|
||||
serializedClaimDetails.taxationType = null;
|
||||
// Fetch default GST rate and taxation type from ActivityType table
|
||||
try {
|
||||
const activityTypeTitle = (claimDetails.activityType || '').trim();
|
||||
logger.info(`[DealerClaimService] Resolving taxationType for activity: "${activityTypeTitle}"`);
|
||||
|
||||
let activity = await ActivityType.findOne({
|
||||
where: { title: activityTypeTitle }
|
||||
});
|
||||
|
||||
// Fallback 1: Try normalized title (handling en-dash vs hyphen)
|
||||
if (!activity && activityTypeTitle) {
|
||||
const normalizedTitle = activityTypeTitle.replace(/–/g, '-');
|
||||
if (normalizedTitle !== activityTypeTitle) {
|
||||
activity = await ActivityType.findOne({
|
||||
where: { title: normalizedTitle }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 2: Handle cases where activity is found but taxationType is missing, or activity not found
|
||||
if (activity && activity.taxationType) {
|
||||
serializedClaimDetails.defaultGstRate = Number(activity.gstRate) || 18;
|
||||
serializedClaimDetails.taxationType = activity.taxationType;
|
||||
logger.info(`[DealerClaimService] Resolved from ActivityType record: ${activity.taxationType}`);
|
||||
} else {
|
||||
// Infer from title if record is missing or incomplete
|
||||
const isNonGst = activityTypeTitle.toLowerCase().includes('non');
|
||||
serializedClaimDetails.taxationType = isNonGst ? 'Non GST' : 'GST';
|
||||
serializedClaimDetails.defaultGstRate = isNonGst ? 0 : (activity ? (Number(activity.gstRate) || 18) : 18);
|
||||
|
||||
logger.info(`[DealerClaimService] Inferred taxationType from title: ${serializedClaimDetails.taxationType} (Activity record ${activity ? 'found but missing taxationType' : 'not found'})`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[DealerClaimService] Error fetching activity type for ${claimDetails.activityType}:`, error);
|
||||
serializedClaimDetails.defaultGstRate = 18;
|
||||
serializedClaimDetails.taxationType = 'GST'; // Safe default
|
||||
}
|
||||
|
||||
|
||||
// Fetch dealer GSTIN from dealers table
|
||||
try {
|
||||
@ -1303,15 +1332,7 @@ export class DealerClaimService {
|
||||
throw new Error('Failed to get proposal ID after saving proposal details');
|
||||
}
|
||||
|
||||
// Save cost items to separate table (preferred approach)
|
||||
if (proposalData.costBreakup && proposalData.costBreakup.length > 0) {
|
||||
// Validate HSN/SAC codes before saving
|
||||
for (const item of proposalData.costBreakup) {
|
||||
// Pass item.isService explicitly; if undefined/null, validation will use default HSN/SAC logic
|
||||
this.validateHSNSAC(item.hsnCode, item.isService);
|
||||
}
|
||||
|
||||
// Delete existing cost items for this proposal (in case of update)
|
||||
// Clear existing cost items for this proposal (in case of update)
|
||||
await DealerProposalCostItem.destroy({
|
||||
where: { proposalId }
|
||||
});
|
||||
@ -1343,7 +1364,6 @@ export class DealerClaimService {
|
||||
|
||||
await DealerProposalCostItem.bulkCreate(costItems);
|
||||
logger.info(`[DealerClaimService] Saved ${costItems.length} cost items for proposal ${proposalId}`);
|
||||
}
|
||||
|
||||
// Update budget tracking with proposal estimate
|
||||
await ClaimBudgetTracking.upsert({
|
||||
@ -1482,9 +1502,12 @@ export class DealerClaimService {
|
||||
|
||||
const completionId = (completionDetails as any)?.completionId;
|
||||
if (completionData.closedExpenses && completionData.closedExpenses.length > 0) {
|
||||
// Validate HSN/SAC codes before saving
|
||||
for (const item of completionData.closedExpenses) {
|
||||
this.validateHSNSAC(item.hsnCode, !!item.isService);
|
||||
// Determine taxation type for fallback logic
|
||||
let isNonGst = false;
|
||||
if (claimDetails?.activityType) {
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
const taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||
isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||
}
|
||||
|
||||
// Clear existing expenses for this request to avoid duplicates
|
||||
@ -1497,8 +1520,8 @@ export class DealerClaimService {
|
||||
// Use provided tax details or calculate if missing/zero
|
||||
let gstRate = Number(item.gstRate);
|
||||
if (isNaN(gstRate) || gstRate === 0) {
|
||||
// Fallback to activity GST rate if available
|
||||
gstRate = 18; // Default fallback
|
||||
// Fallback to activity GST rate ONLY for GST claims
|
||||
gstRate = isNonGst ? 0 : 18;
|
||||
}
|
||||
|
||||
const hasUtgst = (Number(item.utgstRate) > 0 || Number(item.utgstAmt) > 0);
|
||||
@ -3533,44 +3556,5 @@ export class DealerClaimService {
|
||||
return plain;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates HSN or SAC code based on GST rules
|
||||
* @param hsnCode HSN/SAC code string
|
||||
* @param isService Boolean indicating if it's a Service (SAC) or Goods (HSN).
|
||||
* If undefined, it will attempt to infer from the code (internal safety).
|
||||
*/
|
||||
private validateHSNSAC(hsnCode: string | undefined, isService?: boolean): void {
|
||||
if (!hsnCode) return;
|
||||
const cleanCode = String(hsnCode).trim();
|
||||
if (!cleanCode) return;
|
||||
|
||||
if (!/^\d+$/.test(cleanCode)) {
|
||||
throw new Error(`Invalid HSN/SAC code: ${hsnCode}. Code must contain only digits.`);
|
||||
}
|
||||
|
||||
// Infer isService if not provided (safety fallback)
|
||||
const effectiveIsService = isService !== undefined ? !!isService : cleanCode.startsWith('99');
|
||||
|
||||
if (effectiveIsService) {
|
||||
// SAC (Services Accounting Code)
|
||||
if (!cleanCode.startsWith('99')) {
|
||||
throw new Error(`Invalid SAC code: ${hsnCode}. Service codes must start with 99.`);
|
||||
}
|
||||
if (cleanCode.length !== 6) {
|
||||
throw new Error(`Invalid SAC code: ${hsnCode}. SAC codes must be exactly 6 digits.`);
|
||||
}
|
||||
} else {
|
||||
// HSN (Harmonized System of Nomenclature) for Goods
|
||||
// Valid lengths in India: 4, 6, 8
|
||||
const validHSNLengths = [4, 6, 8];
|
||||
if (!validHSNLengths.includes(cleanCode.length)) {
|
||||
throw new Error(`Invalid HSN code: ${hsnCode}. HSN codes must be 4, 6, or 8 digits.`);
|
||||
}
|
||||
if (cleanCode.startsWith('99')) {
|
||||
throw new Error(`Invalid HSN code: ${hsnCode}. HSN codes should not start with 99 (reserved for SAC).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -96,6 +96,11 @@ const BLOCKED_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
||||
pattern: /\.\w+\.(exe|bat|cmd|com|msi|scr|pif|vbs|vbe|js|jse|wsf|wsh|ps1|sh|bash)$/i,
|
||||
reason: 'Suspicious double extension'
|
||||
},
|
||||
// XSS Patterns in filenames
|
||||
{
|
||||
pattern: /<script|javascript:|onerror=|onload=|onclick=|alert\(|eval\(|document\./i,
|
||||
reason: 'Potential XSS payload in filename'
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -218,8 +223,10 @@ export function sanitizeFilename(original: string): string {
|
||||
let name = original.replace(/^.*[\\\/]/, '');
|
||||
// Remove null bytes
|
||||
name = name.replace(/\x00/g, '');
|
||||
// Replace dangerous characters
|
||||
// Replace dangerous characters including XSS-prone characters
|
||||
name = name.replace(/[<>:"|?*\x00-\x1F\x7F]/g, '_');
|
||||
// More aggressive XSS sanitization (replace suspicious keywords)
|
||||
name = name.replace(/(onerror|onload|onclick|onmouseover|onfocus|alert|eval|javascript|vbscript|script|expression|document)/gi, 'safe');
|
||||
// Collapse multiple dots
|
||||
name = name.replace(/\.{2,}/g, '.');
|
||||
// Trim leading/trailing dots and spaces
|
||||
|
||||
@ -5,6 +5,7 @@ import { DealerClaimDetails } from '@models/DealerClaimDetails';
|
||||
import { DealerProposalDetails } from '@models/DealerProposalDetails';
|
||||
import { DealerCompletionExpense } from '@models/DealerCompletionExpense';
|
||||
import { ClaimInvoiceItem } from '@models/ClaimInvoiceItem';
|
||||
import { ActivityType } from '@models/ActivityType';
|
||||
import { amountToWords } from '@utils/currencyUtils';
|
||||
import { findDealerLocally } from './dealer.service';
|
||||
import path from 'path';
|
||||
@ -13,15 +14,9 @@ import logger from '@utils/logger';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export class PdfService {
|
||||
private storagePath = path.join(process.cwd(), 'storage', 'invoices');
|
||||
constructor() { }
|
||||
|
||||
constructor() {
|
||||
if (!fs.existsSync(this.storagePath)) {
|
||||
fs.mkdirSync(this.storagePath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async generateInvoicePdf(requestId: string): Promise<string> {
|
||||
async generateInvoicePdf(requestId: string): Promise<Buffer> {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
@ -44,6 +39,13 @@ export class PdfService {
|
||||
});
|
||||
const dealer = await findDealerLocally(claimDetails?.dealerCode, claimDetails?.dealerEmail);
|
||||
|
||||
// Resolve taxationType
|
||||
let taxationType = 'GST';
|
||||
if (claimDetails?.activityType) {
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails?.activityType } });
|
||||
taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST');
|
||||
}
|
||||
|
||||
if (!request || !invoice) {
|
||||
throw new Error('Request or Invoice not found');
|
||||
}
|
||||
@ -55,24 +57,19 @@ export class PdfService {
|
||||
proposalDetails,
|
||||
completionExpenses,
|
||||
invoiceItems,
|
||||
dealer
|
||||
dealer,
|
||||
taxationType
|
||||
});
|
||||
|
||||
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
|
||||
|
||||
const fileName = `invoice_${requestId}_${Date.now()}.pdf`;
|
||||
const filePath = path.join(this.storagePath, fileName);
|
||||
|
||||
await page.pdf({
|
||||
path: filePath,
|
||||
const pdfBuffer = Buffer.from(await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' }
|
||||
});
|
||||
}));
|
||||
|
||||
await invoice.update({ filePath: fileName });
|
||||
|
||||
return fileName;
|
||||
return pdfBuffer;
|
||||
} catch (error) {
|
||||
logger.error(`[PdfService] Error generating PDF for request ${requestId}:`, error);
|
||||
throw error;
|
||||
@ -82,9 +79,10 @@ export class PdfService {
|
||||
}
|
||||
|
||||
private getInvoiceHtmlTemplate(data: any): string {
|
||||
const { request, invoice, dealer, claimDetails, completionExpenses = [], invoiceItems = [] } = data;
|
||||
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
|
||||
const { request, invoice, dealer, claimDetails, completionExpenses = [], invoiceItems = [], taxationType } = data;
|
||||
const qrImage = invoice.qrImage ? `data:image/png,base64,${invoice.qrImage}` : '';
|
||||
const logoUrl = `{{LOGO_URL}}`;
|
||||
const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
|
||||
|
||||
let tableRows = '';
|
||||
|
||||
@ -94,18 +92,20 @@ export class PdfService {
|
||||
<tr>
|
||||
<td>${item.slNo}</td>
|
||||
<td>${item.description}</td>
|
||||
<td>${item.hsnCd}</td>
|
||||
${!isNonGst ? `<td>${item.hsnCd}</td>` : ''}
|
||||
<td>${Number(item.assAmt).toFixed(2)}</td>
|
||||
<td>0.00</td>
|
||||
<td>${item.unit}</td>
|
||||
<td>${Number(item.assAmt).toFixed(2)}</td>
|
||||
${!isNonGst ? `<td>${Number(item.assAmt).toFixed(2)}</td>` : ''}
|
||||
${!isNonGst ? `
|
||||
<td>${Number(item.igstAmt) > 0 ? Number(item.gstRt).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(item.igstAmt).toFixed(2)}</td>
|
||||
<td>${Number(item.cgstAmt) > 0 ? (Number(item.gstRt) / 2).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(item.cgstAmt).toFixed(2)}</td>
|
||||
<td>${Number(item.sgstAmt) > 0 ? (Number(item.gstRt) / 2).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(item.sgstAmt).toFixed(2)}</td>
|
||||
<td>${Number(item.totItemVal).toFixed(2)}</td>
|
||||
` : ''}
|
||||
<td>${Number(isNonGst ? item.assAmt : item.totItemVal).toFixed(2)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else if (completionExpenses.length > 0) {
|
||||
@ -114,7 +114,7 @@ export class PdfService {
|
||||
completionExpenses.forEach((item: any) => {
|
||||
const hsn = item.hsnCode || 'N/A';
|
||||
const rate = Number(item.gstRate || 0);
|
||||
const key = `${hsn}_${rate}`;
|
||||
const key = isNonGst ? 'NON_GST' : `${hsn}_${rate}`;
|
||||
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {
|
||||
@ -143,18 +143,20 @@ export class PdfService {
|
||||
<tr>
|
||||
<td>EXPENSE</td>
|
||||
<td>${item.description}</td>
|
||||
<td>${item.hsn}</td>
|
||||
${!isNonGst ? `<td>${item.hsn}</td>` : ''}
|
||||
<td>${Number(item.amount).toFixed(2)}</td>
|
||||
<td>0.00</td>
|
||||
<td>EA</td>
|
||||
<td>${Number(item.amount).toFixed(2)}</td>
|
||||
${!isNonGst ? `<td>${Number(item.amount).toFixed(2)}</td>` : ''}
|
||||
${!isNonGst ? `
|
||||
<td>${Number(item.igstRate || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.igstAmt || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.cgstRate || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.cgstAmt || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.sgstRate || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.sgstAmt || 0).toFixed(2)}</td>
|
||||
<td>${Number(item.totalAmt || 0).toFixed(2)}</td>
|
||||
` : ''}
|
||||
<td>${Number(isNonGst ? item.amount : item.totalAmt).toFixed(2)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
@ -162,18 +164,20 @@ export class PdfService {
|
||||
<tr>
|
||||
<td>CLAIM</td>
|
||||
<td>${request.title || 'Warranty Claim'}</td>
|
||||
<td>998881</td>
|
||||
${!isNonGst ? '<td>998881</td>' : ''}
|
||||
<td>${Number(invoice.taxableValue || invoice.amount || 0).toFixed(2)}</td>
|
||||
<td>0.00</td>
|
||||
<td>EA</td>
|
||||
<td>${Number(invoice.taxableValue || invoice.amount || 0).toFixed(2)}</td>
|
||||
${!isNonGst ? `<td>${Number(invoice.taxableValue || invoice.amount || 0).toFixed(2)}</td>` : ''}
|
||||
${!isNonGst ? `
|
||||
<td>${invoice.igstTotal > 0 ? (Number(invoice.igstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(invoice.igstTotal || 0).toFixed(2)}</td>
|
||||
<td>${invoice.cgstTotal > 0 ? (Number(invoice.cgstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(invoice.cgstTotal || 0).toFixed(2)}</td>
|
||||
<td>${invoice.sgstTotal > 0 ? (Number(invoice.sgstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'}</td>
|
||||
<td>${Number(invoice.sgstTotal || 0).toFixed(2)}</td>
|
||||
<td>${Number(invoice.amount || 0).toFixed(2)}</td>
|
||||
` : ''}
|
||||
<td>${Number(isNonGst ? (invoice.taxableValue || invoice.amount) : invoice.amount || 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@ -185,24 +189,34 @@ export class PdfService {
|
||||
if (invoiceItems && invoiceItems.length > 0) {
|
||||
invoiceItems.forEach((item: any) => {
|
||||
totalTaxable += Number(item.assAmt || 0);
|
||||
if (!isNonGst) {
|
||||
totalTax += Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0);
|
||||
grandTotal += Number(item.totItemVal || 0);
|
||||
} else {
|
||||
grandTotal += Number(item.assAmt || 0);
|
||||
}
|
||||
});
|
||||
} else if (completionExpenses.length > 0) {
|
||||
// Fallback for completion expenses calculation
|
||||
completionExpenses.forEach((item: any) => {
|
||||
const qty = Number(item.quantity || 1);
|
||||
const baseAmt = Number(item.amount || 0) * qty;
|
||||
const taxAmt = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0);
|
||||
totalTaxable += baseAmt;
|
||||
if (!isNonGst) {
|
||||
const taxAmt = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0);
|
||||
totalTax += taxAmt;
|
||||
grandTotal += (baseAmt + taxAmt);
|
||||
} else {
|
||||
grandTotal += baseAmt;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback for invoice record
|
||||
totalTaxable = Number(invoice.taxableValue || invoice.amount || 0);
|
||||
if (!isNonGst) {
|
||||
totalTax = Number(invoice.igstTotal || 0) + Number(invoice.cgstTotal || 0) + Number(invoice.sgstTotal || 0);
|
||||
grandTotal = Number(invoice.amount || 0);
|
||||
} else {
|
||||
grandTotal = totalTaxable;
|
||||
}
|
||||
}
|
||||
|
||||
const totalValueInWords = amountToWords(grandTotal);
|
||||
@ -249,7 +263,7 @@ export class PdfService {
|
||||
${qrImage ? `<img src="${qrImage}" class="qr-code" />` : ''}
|
||||
</div>
|
||||
|
||||
<div class="title">WARRANTY CLAIM TAX INVOICE</div>
|
||||
<div class="title">${isNonGst ? 'CLAIM INVOICE' : 'CLAIM TAX INVOICE'}</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-section">
|
||||
@ -275,17 +289,19 @@ export class PdfService {
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Description</th>
|
||||
<th>HSN/SAC</th>
|
||||
<th>Base Amount</th>
|
||||
${!isNonGst ? '<th>HSN/SAC</th>' : ''}
|
||||
<th>${isNonGst ? 'Amount' : 'Base Amount'}</th>
|
||||
<th>Disc</th>
|
||||
<th>UOM</th>
|
||||
<th>Taxable Value</th>
|
||||
${!isNonGst ? '<th>Taxable Value</th>' : ''}
|
||||
${!isNonGst ? `
|
||||
<th>IGST %</th>
|
||||
<th>IGST</th>
|
||||
<th>CGST %</th>
|
||||
<th>CGST</th>
|
||||
<th>SGST %</th>
|
||||
<th>SGST</th>
|
||||
` : ''}
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -295,14 +311,14 @@ export class PdfService {
|
||||
</table>
|
||||
|
||||
<div class="totals">
|
||||
<div class="totals-row"><span>TAXABLE TOTAL:</span><span>${totalTaxable.toFixed(2)}</span></div>
|
||||
<div class="totals-row"><span>TOTAL TAX:</span><span>${totalTax.toFixed(2)}</span></div>
|
||||
<div class="totals-row"><span>${isNonGst ? 'TOTAL AMOUNT' : 'TAXABLE TOTAL'}:</span><span>${totalTaxable.toFixed(2)}</span></div>
|
||||
${!isNonGst ? `<div class="totals-row"><span>TOTAL TAX:</span><span>${totalTax.toFixed(2)}</span></div>` : ''}
|
||||
<div class="totals-row grand-total"><span>GRAND TOTAL:</span><span>${grandTotal.toFixed(2)}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="words">
|
||||
<div><strong>TOTAL VALUE IN WORDS:</strong> Rupees ${totalValueInWords}</div>
|
||||
<div><strong>TOTAL TAX IN WORDS:</strong> Rupees ${totalTaxInWords}</div>
|
||||
${!isNonGst ? `<div><strong>TOTAL TAX IN WORDS:</strong> Rupees ${totalTaxInWords}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
|
||||
@ -54,8 +54,10 @@ export const userIdParamsSchema = z.object({
|
||||
export const createActivityTypeSchema = z.object({
|
||||
title: z.string().min(1, 'Title is required').max(255, 'Title too long'),
|
||||
itemCode: z.string().max(50, 'Item code too long').optional(),
|
||||
taxationType: z.string().max(50, 'Taxation type too long').optional(),
|
||||
sapRefNo: z.string().max(50, 'SAP ref number too long').optional(),
|
||||
taxationType: z.enum(['GST', 'Non GST'], {
|
||||
errorMap: () => ({ message: 'Taxation type must be GST or Non GST' }),
|
||||
}),
|
||||
sapRefNo: z.string().min(1, 'SAP ref number (Claim Document Type) is required').max(50, 'SAP ref number too long'),
|
||||
});
|
||||
|
||||
export const updateActivityTypeSchema = createActivityTypeSchema.partial();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user