import { Router } from 'express'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; import { authenticateToken } from '../middlewares/auth.middleware'; import { requireForm16SubmissionAccess, requireForm1626AsAccess, requireForm16ReOnly } from '../middlewares/form16Permission.middleware'; import { form16Controller } from '../controllers/form16.controller'; import { form16SapController } from '../controllers/form16Sap.controller'; import { asyncHandler } from '../middlewares/errorHandler.middleware'; import { UPLOAD_DIR } from '../config/storage'; const router = Router(); // REform16 pattern: disk storage to uploads dir (path.join(__dirname, '../../uploads') → we use UPLOAD_DIR/form16-extract) const form16ExtractDir = path.join(UPLOAD_DIR, 'form16-extract'); if (!fs.existsSync(form16ExtractDir)) { fs.mkdirSync(form16ExtractDir, { recursive: true }); } const storage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, form16ExtractDir), filename: (_req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); cb(null, `form16a-${uniqueSuffix}${path.extname(file.originalname || '.pdf')}`); }, }); // Same multer config as REform16: disk storage, 10MB, PDF only, field 'document' const uploadExtract = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { const ext = path.extname(file.originalname || '').toLowerCase(); if (ext === '.pdf') { cb(null, true); } else { cb(new Error('Only PDF files are allowed')); } }, }); // Memory storage for submission (needs buffer for GCS upload) const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 15 * 1024 * 1024 }, }); // 26AS upload: .txt only, 5MB, memory storage (parse then bulk insert) const upload26asTxt = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { const ext = path.extname(file.originalname || '').toLowerCase(); const isTxt = ext === '.txt' || (file.mimetype && (file.mimetype === 'text/plain' || file.mimetype === 'application/octet-stream')); if (isTxt) { cb(null, true); } else { cb(new Error('Only .txt files are allowed for 26AS upload')); } }, }); router.use(authenticateToken); // Permissions (API-driven from admin config; used by frontend to show/hide Form 16 and 26AS) router.get( '/permissions', asyncHandler(form16Controller.getPermissions.bind(form16Controller)) ); // Form 16 submission data (who can see: submissionViewerEmails from admin config; dealers always allowed) router.get( '/credit-notes', requireForm16SubmissionAccess, asyncHandler(form16Controller.listCreditNotes.bind(form16Controller)) ); // RE only: list debit notes router.get( '/debit-notes', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.listDebitNotes.bind(form16Controller)) ); router.get( '/debit-notes/:id/sap-response', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.viewDebitNoteSapResponse.bind(form16Controller)) ); router.get( '/credit-notes/:id', requireForm16SubmissionAccess, asyncHandler(form16Controller.getCreditNoteById.bind(form16Controller)) ); router.get( '/credit-notes/:id/download', requireForm16SubmissionAccess, asyncHandler(form16Controller.downloadCreditNote.bind(form16Controller)) ); router.get( '/requests/:requestId/credit-note', requireForm16SubmissionAccess, asyncHandler(form16Controller.getCreditNoteByRequest.bind(form16Controller)) ); // RE only: Form 16 request actions (cancel, resubmission needed) router.post( '/requests/:requestId/cancel-submission', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.cancelForm16Submission.bind(form16Controller)) ); router.post( '/requests/:requestId/resubmission-needed', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.setForm16ResubmissionNeeded.bind(form16Controller)) ); // Dealer: contact admin when 26AS missing for quarter router.post( '/requests/:requestId/contact-admin', requireForm16SubmissionAccess, asyncHandler(form16Controller.contactAdmin.bind(form16Controller)) ); // Pull SAP outgoing responses now (credit/debit). Used by Pull button in UI. router.post( '/sap/pull', requireForm16SubmissionAccess, asyncHandler(form16SapController.pull.bind(form16SapController)) ); // Form 16 SAP simulation (credit note / debit note). Replace with real SAP when integrating. router.post( '/sap-simulate/credit-note', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.sapSimulateCreditNote.bind(form16Controller)) ); router.post( '/sap-simulate/debit-note', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.sapSimulateDebitNote.bind(form16Controller)) ); // Dealer-only: pending submissions and pending quarters (Form 16 Pending Submissions page) router.get( '/dealer/submissions', requireForm16SubmissionAccess, asyncHandler(form16Controller.listDealerSubmissions.bind(form16Controller)) ); router.get( '/dealer/pending-quarters', requireForm16SubmissionAccess, asyncHandler(form16Controller.listDealerPendingQuarters.bind(form16Controller)) ); router.get( '/non-submitted-dealers', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.listNonSubmittedDealers.bind(form16Controller)) ); router.post( '/non-submitted-dealers/notify', requireForm16ReOnly, requireForm16SubmissionAccess, asyncHandler(form16Controller.notifyNonSubmittedDealer.bind(form16Controller)) ); // 26AS (who can see: twentySixAsViewerEmails from admin config) router.get( '/26as', requireForm1626AsAccess, asyncHandler(form16Controller.list26as.bind(form16Controller)) ); router.post( '/26as', requireForm1626AsAccess, asyncHandler(form16Controller.create26as.bind(form16Controller)) ); router.put( '/26as/:id', requireForm1626AsAccess, asyncHandler(form16Controller.update26as.bind(form16Controller)) ); router.delete( '/26as/:id', requireForm1626AsAccess, asyncHandler(form16Controller.delete26as.bind(form16Controller)) ); router.get( '/26as/upload-history', requireForm1626AsAccess, asyncHandler(form16Controller.get26asUploadHistory.bind(form16Controller)) ); router.post( '/26as/upload', requireForm1626AsAccess, upload26asTxt.single('file'), asyncHandler(form16Controller.upload26as.bind(form16Controller)) ); // Extract – dealers/RE with Form 16 submission access router.post( '/extract', requireForm16SubmissionAccess, uploadExtract.single('document'), asyncHandler(form16Controller.extractOcr.bind(form16Controller)) ); // Create Form 16 submission – dealers/RE with Form 16 submission access router.post( '/submissions', requireForm16SubmissionAccess, upload.single('document'), asyncHandler(form16Controller.createSubmission.bind(form16Controller)) ); // Dev only: send a Form 16 test notification (in-app + email via Ethereal) to current user if (process.env.NODE_ENV !== 'production') { router.post( '/test-notification', asyncHandler(form16Controller.testNotification.bind(form16Controller)) ); } export default router;