add uploaded Form16 PDF APIs and bulk download support

Add RE uploaded Form16 PDF listing and bulk download support in backend, including dependency and migration updates required by the new flow.

Made-with: Cursor
This commit is contained in:
Aaditya Jaiswal 2026-04-28 18:55:24 +05:30
parent 8e40b73f65
commit d25ffbaf7b
6 changed files with 1040 additions and 814 deletions

988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@
"@google/generative-ai": "^0.24.1",
"@types/nodemailer": "^7.0.4",
"@types/uuid": "^8.3.4",
"archiver": "^7.0.1",
"axios": "^1.7.9",
"bcryptjs": "^2.4.3",
"bullmq": "^5.63.0",
@ -78,6 +79,7 @@
"zod": "^3.24.1"
},
"devDependencies": {
"@types/archiver": "^7.0.0",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.17",

View File

@ -199,6 +199,58 @@ export class Form16Controller {
}
}
/**
* GET /api/v1/form16/uploaded-form16-pdfs
* RE only. List uploaded Form16A PDFs that are successfully submitted and have credit notes.
*/
async listUploadedForm16Pdfs(req: Request, res: Response): Promise<void> {
try {
const result = await form16Service.listUploadedForm16Pdfs({
search: req.query.search as string | undefined,
dealerCode: req.query.dealerCode as string | undefined,
dealerName: req.query.dealerName as string | undefined,
financialYear: req.query.financialYear as string | undefined,
assessmentYear: req.query.assessmentYear as string | undefined,
quarter: req.query.quarter as string | undefined,
uploadedFrom: req.query.uploadedFrom as string | undefined,
uploadedTo: req.query.uploadedTo as string | undefined,
zone: req.query.zone as string | undefined,
region: req.query.region as string | undefined,
limit: req.query.limit != null ? parseInt(String(req.query.limit), 10) : undefined,
offset: req.query.offset != null ? parseInt(String(req.query.offset), 10) : undefined,
});
return ResponseHandler.success(res, result, 'Uploaded Form 16 PDFs fetched');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[Form16Controller] listUploadedForm16Pdfs error:', error);
return ResponseHandler.error(res, 'Failed to fetch uploaded Form 16 PDFs', 500, errorMessage);
}
}
/**
* POST /api/v1/form16/uploaded-form16-pdfs/bulk-download-links
* RE only. Return document links for selected submission ids (used by FE bulk download action).
*/
async getUploadedForm16PdfBulkDownloadLinks(req: Request, res: Response): Promise<void> {
try {
const idsRaw = Array.isArray((req.body as any)?.submissionIds) ? (req.body as any).submissionIds : [];
const requested = idsRaw
.map((v: unknown) => Number(v))
.filter((n: number) => Number.isInteger(n) && n > 0);
if (!requested.length) {
return ResponseHandler.error(res, 'submissionIds is required', 400);
}
const files = await form16Service.getUploadedForm16PdfLinksBySubmissionIds(requested);
return ResponseHandler.success(res, { files }, 'Bulk download links generated');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[Form16Controller] getUploadedForm16PdfBulkDownloadLinks error:', error);
return ResponseHandler.error(res, 'Failed to prepare bulk download links', 500, errorMessage);
}
}
/**
* GET /api/v1/form16/dealer/submissions
* Dealer only. List Form 16 submissions for the authenticated dealer (pending/failed for Pending Submissions page).
@ -348,6 +400,11 @@ export class Form16Controller {
async get26asDashboard(req: Request, res: Response): Promise<void> {
try {
const dashboard = await form16Service.getForm16DashboardData();
logger.info('[Form16Controller] 26AS dashboard served', {
yearRows: dashboard.yearWise?.length || 0,
zoneRows: dashboard.zoneWise?.length || 0,
zoneHierarchy: dashboard.zoneHierarchy?.length || 0,
});
return ResponseHandler.success(res, dashboard, 'Form16A dashboard fetched');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

View File

@ -0,0 +1,50 @@
import { QueryInterface } from 'sequelize';
module.exports = {
up: async (queryInterface: QueryInterface) => {
// Speeds latest-submission window ordering per dealer+FY+quarter.
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_form16a_submissions_dashboard_latest
ON form16a_submissions (
dealer_code,
financial_year,
quarter,
version DESC,
submitted_date DESC,
created_at DESC,
id DESC
);
`);
// Speeds join between latest submissions and 26AS quarter snapshots.
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_form16a_submissions_tan_fy_quarter
ON form16a_submissions (tan_number, financial_year, quarter);
`);
// Speeds normalized active dealer lookup used by dashboard aggregation.
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_dealers_active_normalized_code_region
ON dealers (
(TRIM(COALESCE(NULLIF(sales_code, ''), NULLIF(dlrcode, '')))),
region
)
WHERE is_active = true;
`);
},
down: async (queryInterface: QueryInterface) => {
await queryInterface.sequelize.query(`
DROP INDEX IF EXISTS idx_dealers_active_normalized_code_region;
`);
await queryInterface.sequelize.query(`
DROP INDEX IF EXISTS idx_form16a_submissions_tan_fy_quarter;
`);
await queryInterface.sequelize.query(`
DROP INDEX IF EXISTS idx_form16a_submissions_dashboard_latest;
`);
},
};

View File

@ -108,6 +108,18 @@ router.get(
requireForm16SubmissionAccess,
asyncHandler(form16Controller.listDebitNotes.bind(form16Controller))
);
router.get(
'/uploaded-form16-pdfs',
requireForm16ReOnly,
requireForm16SubmissionAccess,
asyncHandler(form16Controller.listUploadedForm16Pdfs.bind(form16Controller))
);
router.post(
'/uploaded-form16-pdfs/bulk-download-links',
requireForm16ReOnly,
requireForm16SubmissionAccess,
asyncHandler(form16Controller.getUploadedForm16PdfBulkDownloadLinks.bind(form16Controller))
);
router.get(
'/debit-notes/:id/sap-response',
requireForm16ReOnly,

View File

@ -1836,11 +1836,21 @@ export async function listDealerPendingQuarters(userId: string) {
`SELECT financial_year, quarter, MIN(created_at) AS min_created FROM tds_26as_entries GROUP BY financial_year, quarter`,
{ type: QueryTypes.SELECT }
));
const twentySixAsAmounts = (await sequelize.query<{ financial_year: string; quarter: string; total_amount: number | string }>(
`SELECT financial_year, quarter, COALESCE(SUM(tax_deducted), 0) AS total_amount FROM tds_26as_entries GROUP BY financial_year, quarter`,
{ type: QueryTypes.SELECT }
));
const minDateByKey = new Map<string, Date>();
for (const row of twentySixAsMinDates) {
const key = `${row.financial_year}|${row.quarter}`;
if (row.min_created) minDateByKey.set(key, new Date(row.min_created));
}
const amountByKey = new Map<string, number>();
for (const row of twentySixAsAmounts) {
const key = `${row.financial_year}|${row.quarter}`;
const amount = Number(row.total_amount ?? 0);
amountByKey.set(key, Number.isFinite(amount) ? amount : 0);
}
const dealerSubmissions = await Form16aSubmission.findAll({
where: { dealerCode },
@ -1877,6 +1887,7 @@ export async function listDealerPendingQuarters(userId: string) {
days_remaining: number | null;
days_overdue: number | null;
days_since_26as_uploaded: number | null;
twenty_six_as_amount: number;
}> = [];
const now = new Date();
const oneDayMs = 24 * 60 * 60 * 1000;
@ -1901,6 +1912,7 @@ export async function listDealerPendingQuarters(userId: string) {
const daysSince26AsUploaded = twentySixAsMin
? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs)
: null;
const twentySixAsAmount = amountByKey.get(key) ?? 0;
result.push({
financial_year: financialYear,
quarter,
@ -1913,6 +1925,7 @@ export async function listDealerPendingQuarters(userId: string) {
days_remaining: daysRemaining,
days_overdue: daysOverdue,
days_since_26as_uploaded: daysSince26AsUploaded,
twenty_six_as_amount: twentySixAsAmount,
});
}
result.sort((a, b) => {
@ -1925,6 +1938,282 @@ export async function listDealerPendingQuarters(userId: string) {
return result;
}
export interface Form16UploadedPdfFilters {
search?: string;
dealerCode?: string;
dealerName?: string;
financialYear?: string;
assessmentYear?: string;
quarter?: string;
uploadedFrom?: string;
uploadedTo?: string;
zone?: string;
region?: string;
limit?: number;
offset?: number;
}
export interface Form16UploadedPdfRow {
submissionId: number;
requestId: string;
creditNoteNumber: string;
dealerName: string;
dealerCode: string;
region: string;
zone: string;
financialYear: string;
assessmentYear: string;
quarter: string;
amount: number;
uploadedDate: string | null;
documentUrl: string;
}
type Form16UploadedPdfSqlRow = {
submission_id: number;
request_id: string;
credit_note_number: string;
dealer_name: string | null;
dealer_code: string;
region: string | null;
zone: string | null;
financial_year: string;
assessment_year: string | null;
quarter: string;
amount: string | number;
uploaded_date: Date | string | null;
document_url: string;
};
/**
* RE only. List uploaded Form16A PDFs that were successfully processed and have credit notes.
* Includes dealer + geography columns and supports filter/search for management views.
*/
export async function listUploadedForm16Pdfs(filters?: Form16UploadedPdfFilters): Promise<{
rows: Form16UploadedPdfRow[];
total: number;
}> {
const replacements: Record<string, unknown> = {
limit: Math.min(Math.max(Number(filters?.limit || 50), 1), 500),
offset: Math.max(Number(filters?.offset || 0), 0),
};
const where: string[] = [
`s.validation_status = 'success'`,
`COALESCE(TRIM(s.document_url), '') <> ''`,
];
const search = String(filters?.search || '').trim().toLowerCase();
if (search) {
replacements.search = `%${search}%`;
where.push(
`(
LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :search
OR LOWER(COALESCE(s.dealer_code, '')) LIKE :search
OR LOWER(COALESCE(s.financial_year, '')) LIKE :search
OR LOWER(COALESCE(s.quarter, '')) LIKE :search
OR LOWER(COALESCE(c.credit_note_number, '')) LIKE :search
)`
);
}
const dealerCode = String(filters?.dealerCode || '').trim();
if (dealerCode) {
replacements.dealerCode = `%${dealerCode.toLowerCase()}%`;
where.push(`LOWER(COALESCE(s.dealer_code, '')) LIKE :dealerCode`);
}
const dealerName = String(filters?.dealerName || '').trim();
if (dealerName) {
replacements.dealerName = `%${dealerName.toLowerCase()}%`;
where.push(`LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :dealerName`);
}
const financialYear = normalizeFinancialYear(filters?.financialYear || '') || String(filters?.financialYear || '').trim();
if (financialYear) {
replacements.financialYear = financialYear;
where.push(`s.financial_year = :financialYear`);
}
const quarter = normalizeQuarter(filters?.quarter || '') || String(filters?.quarter || '').trim().toUpperCase();
if (quarter) {
replacements.quarter = quarter;
where.push(`s.quarter = :quarter`);
}
const region = String(filters?.region || '').trim();
if (region) {
replacements.region = `%${region.toLowerCase()}%`;
where.push(`LOWER(COALESCE(d.region, 'UNKNOWN')) LIKE :region`);
}
const zone = String(filters?.zone || '').trim();
if (zone) {
replacements.zone = zone.toUpperCase();
where.push(
`(CASE
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) IN ('N','S','E','W','C')
THEN UPPER(LEFT(d.region, 1))
ELSE 'UNKNOWN'
END) = :zone`
);
}
const uploadedFrom = String(filters?.uploadedFrom || '').trim();
if (uploadedFrom) {
replacements.uploadedFrom = uploadedFrom;
where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) >= :uploadedFrom`);
}
const uploadedTo = String(filters?.uploadedTo || '').trim();
if (uploadedTo) {
replacements.uploadedTo = uploadedTo;
where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) <= :uploadedTo`);
}
const assessmentYear = String(filters?.assessmentYear || '').trim();
if (assessmentYear) {
replacements.assessmentYear = assessmentYear.toLowerCase();
where.push(
`LOWER(
CASE
WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$'
THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0'))
ELSE s.financial_year
END
) LIKE '%' || :assessmentYear || '%'`
);
}
const whereSql = where.length ? `WHERE ${where.join('\n AND ')}` : '';
const baseFromSql = `
FROM form16a_submissions s
INNER JOIN form_16_credit_notes c ON c.submission_id = s.id
LEFT JOIN LATERAL (
SELECT
d.dealership,
d.dealer_principal_name,
d.region
FROM dealers d
WHERE d.is_active = true
AND (
COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '')
OR COALESCE(d.dlrcode, '') = COALESCE(s.dealer_code, '')
)
ORDER BY CASE WHEN COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '') THEN 0 ELSE 1 END
LIMIT 1
) d ON true
`;
const rows = await sequelize.query<Form16UploadedPdfSqlRow>(
`
SELECT
s.id AS submission_id,
s.request_id,
c.credit_note_number,
COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code) AS dealer_name,
s.dealer_code,
COALESCE(d.region, 'UNKNOWN') AS region,
CASE
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'N' THEN 'North'
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'S' THEN 'South'
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'E' THEN 'East'
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'W' THEN 'West'
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'C' THEN 'Central'
ELSE 'Unknown'
END AS zone,
s.financial_year,
CASE
WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$'
THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0'))
ELSE s.financial_year
END AS assessment_year,
s.quarter,
s.total_amount AS amount,
COALESCE(s.submitted_date, s.created_at) AS uploaded_date,
s.document_url
${baseFromSql}
${whereSql}
ORDER BY COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
LIMIT :limit OFFSET :offset
`,
{ replacements, type: QueryTypes.SELECT }
);
const countRows = await sequelize.query<{ total: string }>(
`
SELECT COUNT(1)::text AS total
${baseFromSql}
${whereSql}
`,
{ replacements, type: QueryTypes.SELECT }
);
const total = Number(countRows?.[0]?.total || 0);
return {
rows: rows.map((r) => ({
submissionId: Number(r.submission_id),
requestId: r.request_id,
creditNoteNumber: r.credit_note_number,
dealerName: String(r.dealer_name || r.dealer_code || '—'),
dealerCode: String(r.dealer_code || '—'),
region: String(r.region || 'UNKNOWN'),
zone: String(r.zone || 'Unknown'),
financialYear: String(r.financial_year || ''),
assessmentYear: String(r.assessment_year || r.financial_year || ''),
quarter: String(r.quarter || ''),
amount: Number(r.amount || 0),
uploadedDate: r.uploaded_date ? new Date(r.uploaded_date).toISOString() : null,
documentUrl: String(r.document_url || ''),
})),
total,
};
}
export async function getUploadedForm16PdfLinksBySubmissionIds(submissionIds: number[]): Promise<Array<{
submissionId: number;
creditNoteNumber: string;
dealerCode: string;
quarter: string;
financialYear: string;
documentUrl: string;
}>> {
const ids = (submissionIds || []).map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0);
if (!ids.length) return [];
const rows = await sequelize.query<{
submission_id: number;
credit_note_number: string;
dealer_code: string;
quarter: string;
financial_year: string;
document_url: string;
}>(
`
SELECT
s.id AS submission_id,
c.credit_note_number,
s.dealer_code,
s.quarter,
s.financial_year,
s.document_url
FROM form16a_submissions s
INNER JOIN form_16_credit_notes c ON c.submission_id = s.id
WHERE s.id IN (:ids)
AND s.validation_status = 'success'
AND COALESCE(TRIM(s.document_url), '') <> ''
ORDER BY s.id DESC
`,
{ replacements: { ids }, type: QueryTypes.SELECT }
);
return rows.map((r) => ({
submissionId: Number(r.submission_id),
creditNoteNumber: r.credit_note_number,
dealerCode: r.dealer_code,
quarter: r.quarter,
financialYear: r.financial_year,
documentUrl: r.document_url,
}));
}
/**
* RE only. Cancel a Form 16 submission: set submission status to cancelled and workflow to REJECTED.
*/
@ -3118,11 +3407,31 @@ export interface Form16DashboardBreakdownRow {
pendingDealerCount: number;
}
export interface Form16DashboardDealerRow {
dealerCode: string;
dealerName: string;
regionId: string;
totalAmount: number;
submittedAmount: number;
pendingAmount: number;
}
export interface Form16DashboardZoneRegionNode extends Form16DashboardBreakdownRow {
regionId: string;
dealers: Form16DashboardDealerRow[];
}
export interface Form16DashboardZoneNode extends Form16DashboardBreakdownRow {
zoneCode: string;
regions: Form16DashboardZoneRegionNode[];
}
export interface Form16DashboardData {
kpi: Form16DashboardKpi;
overall: Form16DashboardOverall;
yearWise: Form16DashboardBreakdownRow[];
zoneWise: Form16DashboardBreakdownRow[];
zoneHierarchy: Form16DashboardZoneNode[];
}
/**
@ -3139,64 +3448,172 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
return Number.isFinite(n) ? n : 0;
};
const [overallRow] = await sequelize.query<{
total_amount: number | string | null;
submitted_amount: number | string | null;
total_dealers: number | string | null;
submitted_dealer_count: number | string | null;
const zoneSortRank = (zoneCode: string): number => {
const z = String(zoneCode || '').toUpperCase();
if (z === 'N') return 1;
if (z === 'C') return 2;
if (z === 'W') return 3;
if (z === 'E') return 4;
if (z === 'S') return 5;
return 99;
};
const zoneName = (zoneCode: string): string => {
const z = String(zoneCode || '').toUpperCase();
if (z === 'N') return 'North';
if (z === 'C') return 'Central';
if (z === 'W') return 'West';
if (z === 'E') return 'East';
if (z === 'S') return 'South';
return z || 'Unknown';
};
const regionSortValue = (regionId: string): number => {
const r = String(regionId || '').toUpperCase();
const m = /^([A-Z]+)(\d+)$/.exec(r);
if (!m) return Number.MAX_SAFE_INTEGER;
return parseInt(m[2], 10);
};
// Dealer master is the base universe for dashboard (include all active dealers).
const dealerUniverseRaw = await sequelize.query<{
dealer_code: string;
dealer_name: string | null;
region_id: string | null;
}>(
`
WITH active_dealers AS (
SELECT DISTINCT
TRIM(COALESCE(d.dlrcode, '')) AS dealer_code,
COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name,
UPPER(TRIM(COALESCE(d.region, ''))) AS region_id
FROM dealers d
WHERE d.is_active = true
AND TRIM(COALESCE(d.dlrcode, '')) <> ''
`,
{ type: QueryTypes.SELECT }
);
const dealerUniverse = (dealerUniverseRaw || []).map((r) => ({
dealerCode: String(r.dealer_code || '').trim(),
dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer',
regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN',
}));
const detailRowsRaw = await sequelize.query<{
dealer_code: string;
dealer_name: string | null;
region_id: string | null;
financial_year: string;
quarter: string;
total_amount: number | string | null;
submitted_amount: number | string | null;
}>(
`
WITH dealer_meta AS (
SELECT DISTINCT
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
TRIM(COALESCE(d.dlrcode, '')) AS dealer_code,
COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name,
UPPER(TRIM(COALESCE(d.region, ''))) AS region_id
FROM dealers d
WHERE d.is_active = true
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
AND TRIM(COALESCE(d.dlrcode, '')) <> ''
),
latest_submissions AS (
SELECT
s.id,
s.dealer_code,
TRIM(COALESCE(s.dealer_code, '')) AS dealer_code,
s.financial_year,
s.quarter,
s.tan_number,
COALESCE(s.total_amount, 0)::numeric AS total_amount,
ROW_NUMBER() OVER (
PARTITION BY s.dealer_code, s.financial_year, s.quarter
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
) AS rn
FROM form16a_submissions s
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
WHERE TRIM(COALESCE(s.dealer_code, '')) <> ''
),
latest_base AS (
SELECT id, dealer_code, financial_year, quarter, total_amount
SELECT id, dealer_code, financial_year, quarter, tan_number, total_amount
FROM latest_submissions
WHERE rn = 1
),
submitted_by_dealer AS (
SELECT
lb.dealer_code,
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
FROM latest_base lb
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
GROUP BY lb.dealer_code
latest_snapshots AS (
SELECT tan_number, financial_year, quarter, aggregated_amount
FROM (
SELECT
ss.tan_number,
ss.financial_year,
ss.quarter,
COALESCE(ss.aggregated_amount, 0)::numeric AS aggregated_amount,
ROW_NUMBER() OVER (
PARTITION BY ss.tan_number, ss.financial_year, ss.quarter
ORDER BY ss.id DESC, ss.created_at DESC
) AS rn
FROM form_16_26as_quarter_snapshots ss
) t
WHERE t.rn = 1
),
credit_by_submission AS (
SELECT cn.submission_id, SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
FROM form_16_credit_notes cn
GROUP BY cn.submission_id
)
SELECT
COALESCE((SELECT SUM(lb.total_amount) FROM latest_base lb), 0) AS total_amount,
COALESCE((SELECT SUM(sbd.submitted_amount) FROM submitted_by_dealer sbd), 0) AS submitted_amount,
COALESCE((SELECT COUNT(*) FROM active_dealers), 0) AS total_dealers,
COALESCE((
SELECT COUNT(DISTINCT sbd.dealer_code)
FROM submitted_by_dealer sbd
WHERE sbd.submitted_amount > 0
), 0) AS submitted_dealer_count
lb.dealer_code,
COALESCE(dm.dealer_name, lb.dealer_code) AS dealer_name,
COALESCE(dm.region_id, 'UNKNOWN') AS region_id,
lb.financial_year,
lb.quarter,
COALESCE(ls.aggregated_amount, lb.total_amount, 0)::numeric AS total_amount,
COALESCE(cbs.submitted_amount, 0)::numeric AS submitted_amount
FROM latest_base lb
LEFT JOIN dealer_meta dm ON dm.dealer_code = lb.dealer_code
LEFT JOIN latest_snapshots ls
ON ls.tan_number = lb.tan_number
AND ls.financial_year = lb.financial_year
AND ls.quarter = lb.quarter
LEFT JOIN credit_by_submission cbs ON cbs.submission_id = lb.id
`,
{ type: QueryTypes.SELECT }
);
const totalAmount = toNum(overallRow?.total_amount);
const submittedAmount = toNum(overallRow?.submitted_amount);
const totalDealers = Math.max(0, Math.trunc(toNum(overallRow?.total_dealers)));
const submittedDealerCount = Math.max(0, Math.trunc(toNum(overallRow?.submitted_dealer_count)));
const detailRows = (detailRowsRaw || []).map((r) => ({
dealerCode: String(r.dealer_code || '').trim(),
dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer',
regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN',
financialYear: String(r.financial_year || '').trim(),
quarter: String(r.quarter || '').trim(),
totalAmount: Math.max(0, toNum(r.total_amount)),
submittedAmount: Math.max(0, toNum(r.submitted_amount)),
}));
const dealerAgg = new Map<string, { dealerName: string; regionId: string; totalAmount: number; submittedAmount: number }>();
for (const d of dealerUniverse) {
dealerAgg.set(d.dealerCode, {
dealerName: d.dealerName,
regionId: d.regionId,
totalAmount: 0,
submittedAmount: 0,
});
}
for (const r of detailRows) {
const key = r.dealerCode;
const prev = dealerAgg.get(key) || {
dealerName: r.dealerName || key,
regionId: r.regionId || 'UNKNOWN',
totalAmount: 0,
submittedAmount: 0,
};
prev.totalAmount += r.totalAmount;
prev.submittedAmount += r.submittedAmount;
dealerAgg.set(key, prev);
}
const totalAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.totalAmount, 0);
const submittedAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.submittedAmount, 0);
const totalDealers = dealerAgg.size;
const submittedDealerCount = Array.from(dealerAgg.values()).filter((r) => r.submittedAmount > 0).length;
const pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount);
const pendingAmount = Math.max(0, totalAmount - submittedAmount);
@ -3205,153 +3622,136 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
return Math.max(0, Math.min(100, Math.round((part / whole) * 100)));
};
const yearRowsRaw = await sequelize.query<{
label: string;
total_amount: number | string | null;
dealer_count: number | string | null;
submitted_amount: number | string | null;
submitted_dealer_count: number | string | null;
}>(
`
WITH active_dealers AS (
SELECT DISTINCT
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
FROM dealers d
WHERE d.is_active = true
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
),
latest_submissions AS (
SELECT
s.id,
s.dealer_code,
s.financial_year,
s.quarter,
COALESCE(s.total_amount, 0)::numeric AS total_amount,
ROW_NUMBER() OVER (
PARTITION BY s.dealer_code, s.financial_year, s.quarter
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
) AS rn
FROM form16a_submissions s
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
),
latest_base AS (
SELECT id, dealer_code, financial_year, quarter, total_amount
FROM latest_submissions
WHERE rn = 1
),
by_year AS (
SELECT
lb.financial_year AS label,
SUM(lb.total_amount)::numeric AS total_amount,
COUNT(DISTINCT lb.dealer_code) AS dealer_count,
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN lb.dealer_code END) AS submitted_dealer_count
FROM latest_base lb
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
GROUP BY lb.financial_year
)
SELECT * FROM by_year
ORDER BY label DESC
`,
{ type: QueryTypes.SELECT }
);
const yearMap = new Map<string, { totalAmount: number; submittedAmount: number; dealers: Set<string>; submittedDealers: Set<string> }>();
for (const r of detailRows) {
const y = r.financialYear || 'Unknown';
const ag = yearMap.get(y) || { totalAmount: 0, submittedAmount: 0, dealers: new Set<string>(), submittedDealers: new Set<string>() };
ag.totalAmount += r.totalAmount;
ag.submittedAmount += r.submittedAmount;
ag.dealers.add(r.dealerCode);
if (r.submittedAmount > 0) ag.submittedDealers.add(r.dealerCode);
yearMap.set(y, ag);
}
const yearWise = Array.from(yearMap.entries())
.sort((a, b) => b[0].localeCompare(a[0]))
.map(([label, ag]) => {
const dealerCount = ag.dealers.size;
const submittedDealerCount = ag.submittedDealers.size;
return {
label,
totalAmount: ag.totalAmount,
dealerCount,
submittedAmount: ag.submittedAmount,
submittedDealerCount,
pendingAmount: Math.max(0, ag.totalAmount - ag.submittedAmount),
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
};
});
const zoneRowsRaw = await sequelize.query<{
label: string;
total_amount: number | string | null;
dealer_count: number | string | null;
submitted_amount: number | string | null;
submitted_dealer_count: number | string | null;
}>(
`
WITH active_dealers AS (
SELECT DISTINCT
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code,
CASE
WHEN UPPER(COALESCE(d.region, '')) LIKE 'N%' THEN 'North'
WHEN UPPER(COALESCE(d.region, '')) LIKE 'S%' THEN 'South'
WHEN UPPER(COALESCE(d.region, '')) LIKE 'E%' THEN 'East'
WHEN UPPER(COALESCE(d.region, '')) LIKE 'W%' THEN 'West'
WHEN UPPER(COALESCE(d.region, '')) LIKE 'C%' THEN 'Central'
ELSE 'Unknown'
END AS zone
FROM dealers d
WHERE d.is_active = true
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
),
latest_submissions AS (
SELECT
s.id,
s.dealer_code,
COALESCE(s.total_amount, 0)::numeric AS total_amount,
ROW_NUMBER() OVER (
PARTITION BY s.dealer_code, s.financial_year, s.quarter
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
) AS rn
FROM form16a_submissions s
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
),
latest_base AS (
SELECT id, dealer_code, total_amount
FROM latest_submissions
WHERE rn = 1
),
by_zone AS (
SELECT
ad.zone AS label,
SUM(COALESCE(lb.total_amount, 0))::numeric AS total_amount,
COUNT(DISTINCT ad.dealer_code) AS dealer_count,
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN ad.dealer_code END) AS submitted_dealer_count
FROM active_dealers ad
LEFT JOIN latest_base lb ON lb.dealer_code = ad.dealer_code
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
GROUP BY ad.zone
)
SELECT * FROM by_zone
ORDER BY CASE label
WHEN 'North' THEN 1
WHEN 'Central' THEN 2
WHEN 'West' THEN 3
WHEN 'East' THEN 4
WHEN 'South' THEN 5
ELSE 99
END, label
`,
{ type: QueryTypes.SELECT }
);
const dealerRegionMap = new Map<string, { dealerName: string; regionId: string; totalAmount: number; submittedAmount: number }>();
for (const [dealerCode, v] of dealerAgg.entries()) {
dealerRegionMap.set(dealerCode, {
dealerName: v.dealerName,
regionId: v.regionId,
totalAmount: v.totalAmount,
submittedAmount: v.submittedAmount,
});
}
const yearWise = (yearRowsRaw || []).map((r) => {
const totalAmountRow = toNum(r.total_amount);
const submittedAmountRow = toNum(r.submitted_amount);
const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
return {
label: r.label,
totalAmount: totalAmountRow,
dealerCount: dealerCountRow,
submittedAmount: submittedAmountRow,
submittedDealerCount: submittedDealerCountRow,
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
const zoneMap = new Map<string, { totalAmount: number; submittedAmount: number; dealers: Set<string>; submittedDealers: Set<string>; regions: Map<string, Form16DashboardZoneRegionNode> }>();
for (const [dealerCode, d] of dealerRegionMap.entries()) {
const regionId = String(d.regionId || '').toUpperCase();
const zoneCode = (/^[A-Z]+/.exec(regionId)?.[0] || 'UNKNOWN').toUpperCase();
const zone = zoneMap.get(zoneCode) || {
totalAmount: 0,
submittedAmount: 0,
dealers: new Set<string>(),
submittedDealers: new Set<string>(),
regions: new Map<string, Form16DashboardZoneRegionNode>(),
};
});
zone.totalAmount += d.totalAmount;
zone.submittedAmount += d.submittedAmount;
zone.dealers.add(dealerCode);
if (d.submittedAmount > 0) zone.submittedDealers.add(dealerCode);
const zoneWise = (zoneRowsRaw || []).map((r) => {
const totalAmountRow = toNum(r.total_amount);
const submittedAmountRow = toNum(r.submitted_amount);
const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
return {
label: r.label,
totalAmount: totalAmountRow,
dealerCount: dealerCountRow,
submittedAmount: submittedAmountRow,
submittedDealerCount: submittedDealerCountRow,
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
const regionNode = zone.regions.get(regionId) || {
regionId,
label: regionId,
totalAmount: 0,
dealerCount: 0,
submittedAmount: 0,
submittedDealerCount: 0,
pendingAmount: 0,
pendingDealerCount: 0,
dealers: [],
};
});
regionNode.totalAmount += d.totalAmount;
regionNode.submittedAmount += d.submittedAmount;
regionNode.dealers.push({
dealerCode,
dealerName: d.dealerName || dealerCode,
regionId,
totalAmount: d.totalAmount,
submittedAmount: d.submittedAmount,
pendingAmount: Math.max(0, d.totalAmount - d.submittedAmount),
});
zone.regions.set(regionId, regionNode);
zoneMap.set(zoneCode, zone);
}
const zoneHierarchy: Form16DashboardZoneNode[] = Array.from(zoneMap.entries())
.map(([zoneCode, z]) => {
const regions = Array.from(z.regions.values())
.map((r) => {
const submittedDealerCount = r.dealers.filter((d) => d.submittedAmount > 0).length;
const dealerCount = r.dealers.length;
const dealers = [...r.dealers].sort((a, b) => a.dealerCode.localeCompare(b.dealerCode));
return {
...r,
dealers,
dealerCount,
submittedDealerCount,
pendingAmount: Math.max(0, r.totalAmount - r.submittedAmount),
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
};
})
.sort((a, b) => {
const da = regionSortValue(a.regionId);
const db = regionSortValue(b.regionId);
if (da !== db) return da - db;
return a.regionId.localeCompare(b.regionId);
});
const dealerCount = z.dealers.size;
const submittedDealerCount = z.submittedDealers.size;
return {
zoneCode,
label: zoneName(zoneCode),
totalAmount: z.totalAmount,
dealerCount,
submittedAmount: z.submittedAmount,
submittedDealerCount,
pendingAmount: Math.max(0, z.totalAmount - z.submittedAmount),
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
regions,
};
})
.sort((a, b) => {
const ra = zoneSortRank(a.zoneCode);
const rb = zoneSortRank(b.zoneCode);
if (ra !== rb) return ra - rb;
return a.label.localeCompare(b.label);
});
const zoneWise = zoneHierarchy.map((z) => ({
label: z.label,
totalAmount: z.totalAmount,
dealerCount: z.dealerCount,
submittedAmount: z.submittedAmount,
submittedDealerCount: z.submittedDealerCount,
pendingAmount: z.pendingAmount,
pendingDealerCount: z.pendingDealerCount,
}));
return {
kpi: {
@ -3370,5 +3770,6 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
},
yearWise,
zoneWise,
zoneHierarchy,
};
}