230 lines
7.2 KiB
TypeScript
230 lines
7.2 KiB
TypeScript
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;
|