/** * Form 16 API – credit notes and (later) submissions. * Uses apiClient for JSON; uses fetch for FormData (so browser sets multipart boundary). */ import apiClient from './authApi'; import { TokenManager } from '@/utils/tokenManager'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'; /** * REform16 pattern: authenticated request; do NOT set Content-Type for FormData (browser sets multipart/form-data + boundary). */ async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<{ data?: unknown; message?: string; success?: boolean }> { const token = TokenManager.getAccessToken(); const headers: HeadersInit = { ...(options.headers || {}), }; if (!(options.body instanceof FormData)) { (headers as Record)['Content-Type'] = 'application/json'; } if (token) { (headers as Record)['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE_URL}${url}`, { ...options, headers, credentials: 'include', }); const contentType = response.headers.get('content-type'); const isJson = contentType?.includes('application/json'); const data = isJson ? await response.json() : { message: await response.text() || 'Request failed' }; if (!response.ok) { const err = new Error((data as { message?: string }).message || `Request failed ${response.status}`) as Error & { response?: { status: number; data?: unknown } }; err.response = { status: response.status, data }; throw err; } return data as { data?: unknown; message?: string; success?: boolean }; } export interface Form16CreditNoteSubmission { requestId: string; form16aNumber: string | null; financialYear: string | null; quarter: string | null; status: string | null; } export interface Form16CreditNoteItem { id: string; creditNoteNumber: string | null; sapDocumentNumber: string | null; amount: number | null; issueDate: string | null; financialYear: string | null; quarter: string | null; status: string | null; remarks: string | null; submission: Form16CreditNoteSubmission | null; /** RE view: dealer code (when listing all credit notes) */ dealerCode?: string | null; dealerName?: string | null; } export interface ListCreditNotesSummary { totalCreditNotes: number; totalAmount: number; activeDealersCount: number; } export interface ListCreditNotesResponse { creditNotes: Form16CreditNoteItem[]; total: number; summary?: ListCreditNotesSummary; } export interface ListCreditNotesParams { financialYear?: string; quarter?: string; } export interface Form16Permissions { canViewForm16Submission: boolean; canView26AS: boolean; } /** * Get Form 16 permissions for the current user (API-driven from admin config). * Use to show/hide Form 16 menu, 26AS, and submission data in the UI. */ export async function getForm16Permissions(): Promise { const res = await apiClient.get<{ data: Form16Permissions }>('/form16/permissions'); const data = res.data?.data ?? res.data; return { canViewForm16Submission: !!data?.canViewForm16Submission, canView26AS: !!data?.canView26AS, }; } /** * List credit notes for the authenticated dealer (resolved by backend from user email). */ export async function listCreditNotes(params?: ListCreditNotesParams): Promise { const searchParams = new URLSearchParams(); if (params?.financialYear) searchParams.set('financialYear', params.financialYear); if (params?.quarter) searchParams.set('quarter', params.quarter); const query = searchParams.toString(); const url = query ? `/form16/credit-notes?${query}` : '/form16/credit-notes'; const { data } = await apiClient.get<{ data?: ListCreditNotesResponse; creditNotes?: Form16CreditNoteItem[]; total?: number; summary?: ListCreditNotesSummary }>(url); const payload = data?.data ?? data; return { creditNotes: payload?.creditNotes ?? [], total: payload?.total ?? 0, summary: payload?.summary, }; } /** Get credit note linked to a Form 16 request (for workflow tab). */ export async function getCreditNoteByRequestId(requestId: string): Promise { const { data } = await apiClient.get<{ data?: { creditNote?: Form16CreditNoteItem | null }; creditNote?: Form16CreditNoteItem | null }>(`/form16/requests/${encodeURIComponent(requestId)}/credit-note`); const payload = data?.data ?? data; return payload?.creditNote ?? null; } /** RE only. Cancel Form 16 submission and mark workflow rejected. */ export async function cancelForm16Submission(requestId: string): Promise { await apiClient.post(`/form16/requests/${encodeURIComponent(requestId)}/cancel-submission`); } /** RE only. Mark Form 16 submission as resubmission needed. */ export async function setForm16ResubmissionNeeded(requestId: string): Promise { await apiClient.post(`/form16/requests/${encodeURIComponent(requestId)}/resubmission-needed`); } /** RE only. Manually generate credit note for Form 16 request. Body: { amount: number }. */ export async function generateForm16CreditNoteManually(requestId: string, amount: number): Promise<{ creditNote: Form16CreditNoteItem }> { const { data } = await apiClient.post<{ data?: { creditNote?: Form16CreditNoteItem }; creditNote?: Form16CreditNoteItem }>( `/form16/requests/${encodeURIComponent(requestId)}/generate-credit-note`, { amount } ); const payload = data?.data ?? data; const creditNote = payload?.creditNote; if (!creditNote) throw new Error('Credit note not returned'); return { creditNote }; } /** Get a single credit note by id with dealer info and dealer transaction history. */ export interface CreditNoteDetailResponse { creditNote: Form16CreditNoteItem & { submission?: { submittedDate?: string | null } }; dealerName: string; dealerEmail: string; dealerContact: string; /** Present when a debit note was issued for this credit note */ debitNote?: Form16DebitNoteItem | null; dealerCreditNotes: Array<{ id: number; creditNoteNumber: string | null; amount: number | null; issueDate: string | null; status: string | null; form16aNumber?: string | null; submittedDate?: string | null; }>; } export async function getCreditNoteById(id: number): Promise { const { data } = await apiClient.get<{ data?: CreditNoteDetailResponse } | CreditNoteDetailResponse>(`/form16/credit-notes/${id}`); const payload = (data && typeof data === 'object' && 'data' in data ? data.data : data) as CreditNoteDetailResponse | undefined; if (!payload?.creditNote) throw new Error('Credit note not found'); return payload; } // ---------- Form 16 SAP simulation (replace with real SAP when integrating) ---------- /** Dealer details for SAP credit note simulation */ export interface SapCreditNoteDealerDetails { dealerCode: string; dealerName?: string | null; dealerEmail?: string | null; dealerContact?: string | null; } /** Simulated SAP credit note response (JSON from SAP) */ export interface SapCreditNoteResponse { success: boolean; creditNoteNumber: string; sapDocumentNumber: string; amount: number; issueDate: string; financialYear?: string; quarter?: string; status: string; message?: string; } /** Dealer info for SAP debit note simulation */ export interface SapDebitNoteDealerInfo { dealerCode: string; dealerName?: string | null; dealerEmail?: string | null; dealerContact?: string | null; } /** Simulated SAP debit note response (JSON from SAP) */ export interface SapDebitNoteResponse { success: boolean; debitNoteNumber: string; sapDocumentNumber: string; amount: number; issueDate: string; status: string; creditNoteNumber: string; message?: string; } /** Simulate SAP credit note generation (Form 16). Returns simulated SAP JSON. */ export async function sapSimulateCreditNote( dealerDetails: SapCreditNoteDealerDetails, amount: number ): Promise { const { data } = await apiClient.post<{ data?: SapCreditNoteResponse } | SapCreditNoteResponse>( '/form16/sap-simulate/credit-note', { dealerDetails, amount } ); const payload = (data && typeof data === 'object' && 'data' in data ? data.data : data) as SapCreditNoteResponse | undefined; if (!payload?.creditNoteNumber) throw new Error('Simulated credit note not returned'); return payload; } /** Simulate SAP debit note generation (Form 16). Returns simulated SAP JSON. */ export async function sapSimulateDebitNote(params: { dealerCode: string; dealerInfo?: SapDebitNoteDealerInfo; creditNoteNumber: string; amount: number; }): Promise { const { data } = await apiClient.post<{ data?: SapDebitNoteResponse } | SapDebitNoteResponse>( '/form16/sap-simulate/debit-note', params ); const payload = (data && typeof data === 'object' && 'data' in data ? data.data : data) as SapDebitNoteResponse | undefined; if (!payload?.debitNoteNumber) throw new Error('Simulated debit note not returned'); return payload; } /** Debit note item (from generate-debit-note or credit note detail). */ export interface Form16DebitNoteItem { id: number; debitNoteNumber: string | null; sapDocumentNumber?: string | null; amount: number | null; issueDate: string | null; status: string | null; reason?: string | null; } /** RE only. Generate debit note for a credit note (Form 16). Calls SAP simulation then saves. */ export async function generateForm16DebitNote( creditNoteId: number, amount: number ): Promise<{ debitNote: Form16DebitNoteItem; creditNote: Form16CreditNoteItem }> { const { data } = await apiClient.post<{ data?: { debitNote?: Form16DebitNoteItem; creditNote?: Form16CreditNoteItem }; debitNote?: Form16DebitNoteItem; creditNote?: Form16CreditNoteItem; }>(`/form16/credit-notes/${creditNoteId}/generate-debit-note`, { amount }); const payload = data?.data ?? data; const debitNote = payload?.debitNote; const creditNote = payload?.creditNote; if (!debitNote) throw new Error('Debit note not returned'); return { debitNote, creditNote: creditNote! }; } /** Extracted Form 16A data from OCR (Gemini or regex fallback) */ export interface Form16AExtractedData { nameAndAddressOfDeductor?: string | null; deductorName?: string | null; deductorAddress?: string | null; deductorPhone?: string | null; deductorEmail?: string | null; totalAmountPaid?: number | null; totalTaxDeducted?: number | null; totalTdsDeposited?: number | null; tanOfDeductor?: string | null; natureOfPayment?: string | null; transactionDate?: string | null; statusOfMatchingOltas?: string | null; dateOfBooking?: string | null; assessmentYear?: string | null; quarter?: string | null; form16aNumber?: string | null; financialYear?: string | null; certificateDate?: string | null; tanNumber?: string | null; tdsAmount?: number | null; totalAmount?: number | null; } export interface ExtractOcrResponse { extractedData: Form16AExtractedData; ocrProvider?: string; } /** * Extract Form 16A data from PDF – exact REform16: FormData with 'document', fetchWithAuth (no Content-Type for FormData). */ export async function extractOcr(file: File): Promise { const formData = new FormData(); formData.append('document', file); const result = await fetchWithAuth('/form16/extract', { method: 'POST', body: formData, }); const raw = result as { data?: { extractedData?: Form16AExtractedData; ocrProvider?: string }; extractedData?: Form16AExtractedData; ocrProvider?: string }; const payload = raw?.data ?? raw; const extracted = payload?.extractedData; const ocrProvider = payload?.ocrProvider; if (!extracted) { throw new Error((result as { message?: string }).message || 'No extracted data returned'); } return { extractedData: extracted, ocrProvider }; } export interface CreateForm16SubmissionPayload { financialYear: string; quarter: string; form16aNumber: string; tdsAmount: number; totalAmount: number; tanNumber: string; deductorName: string; version?: number; file: File; /** Optional: raw OCR extracted data for audit (stored in form16a_submissions.ocr_extracted_data). */ extractedData?: Form16AExtractedData | null; } export interface CreateForm16SubmissionResponse { requestId: string; requestNumber: string; submissionId: number; /** Set when 26AS matching runs: 'success' | 'failed' | 'resubmission_needed' */ validationStatus?: string; /** Credit note number when validationStatus === 'success' */ creditNoteNumber?: string | null; } /** * Create Form 16 submission (workflow request + form16a_submissions + document upload). * Uses fetch + FormData so browser sets Content-Type with boundary (avoids 400). */ export async function createForm16Submission(payload: CreateForm16SubmissionPayload): Promise { const formData = new FormData(); formData.append('document', payload.file); formData.append('financialYear', payload.financialYear); formData.append('quarter', payload.quarter); formData.append('form16aNumber', payload.form16aNumber); formData.append('tdsAmount', String(payload.tdsAmount)); formData.append('totalAmount', String(payload.totalAmount)); formData.append('tanNumber', payload.tanNumber); formData.append('deductorName', payload.deductorName); if (payload.version != null) formData.append('version', String(payload.version)); if (payload.extractedData != null) { formData.append('ocrExtractedData', JSON.stringify(payload.extractedData)); } const body = await fetchWithAuth('/form16/submissions', { method: 'POST', body: formData }); const result = (body as { data?: CreateForm16SubmissionResponse }).data ?? body; const req = result as CreateForm16SubmissionResponse; if (!req?.requestNumber) { throw new Error((body as { message?: string }).message || 'Invalid response from server'); } return req; } // ---------- RE: 26AS ---------- export interface Tds26asEntryItem { id: number; tanNumber: string; deductorName?: string | null; quarter: string; assessmentYear?: string | null; financialYear: string; sectionCode?: string | null; amountPaid?: number | null; taxDeducted: number; totalTdsDeposited?: number | null; natureOfPayment?: string | null; transactionDate?: string | null; dateOfBooking?: string | null; statusOltas?: string | null; remarks?: string | null; createdAt?: string; updatedAt?: string; } export interface List26asParams { financialYear?: string; quarter?: string; tanNumber?: string; search?: string; status?: string; assessmentYear?: string; sectionCode?: string; limit?: number; offset?: number; } export interface List26asSummary { totalRecords: number; booked: number; notBooked: number; pending: number; totalTaxDeducted: number; } export async function list26asEntries(params?: List26asParams): Promise<{ entries: Tds26asEntryItem[]; total: number; summary: List26asSummary; }> { const searchParams = new URLSearchParams(); if (params?.financialYear) searchParams.set('financialYear', params.financialYear); if (params?.quarter) searchParams.set('quarter', params.quarter); if (params?.tanNumber) searchParams.set('tanNumber', params.tanNumber); if (params?.search) searchParams.set('search', params.search); if (params?.status) searchParams.set('status', params.status); if (params?.assessmentYear) searchParams.set('assessmentYear', params.assessmentYear); if (params?.sectionCode) searchParams.set('sectionCode', params.sectionCode); if (params?.limit != null) searchParams.set('limit', String(params.limit)); if (params?.offset != null) searchParams.set('offset', String(params.offset)); const query = searchParams.toString(); const url = query ? `/form16/26as?${query}` : '/form16/26as'; const { data } = await apiClient.get<{ data?: { entries?: Tds26asEntryItem[]; total?: number; summary?: List26asSummary }; entries?: Tds26asEntryItem[]; total?: number; summary?: List26asSummary; }>(url); const payload = data?.data ?? data; return { entries: payload?.entries ?? [], total: payload?.total ?? 0, summary: payload?.summary ?? { totalRecords: 0, booked: 0, notBooked: 0, pending: 0, totalTaxDeducted: 0, }, }; } export async function create26asEntry(body: Partial): Promise<{ entry: Tds26asEntryItem }> { const { data } = await apiClient.post<{ data?: { entry?: Tds26asEntryItem }; entry?: Tds26asEntryItem }>('/form16/26as', body); const payload = data?.data ?? data; const entry = (payload?.entry ?? payload) as Tds26asEntryItem | undefined; if (!entry || typeof entry !== 'object') throw new Error('No entry returned'); return { entry }; } export async function update26asEntry(id: number, body: Partial): Promise<{ entry: Tds26asEntryItem }> { const { data } = await apiClient.put<{ data?: { entry?: Tds26asEntryItem }; entry?: Tds26asEntryItem }>(`/form16/26as/${id}`, body); const payload = data?.data ?? data; const entry = (payload?.entry ?? payload) as Tds26asEntryItem | undefined; if (!entry || typeof entry !== 'object') throw new Error('No entry returned'); return { entry }; } export async function delete26asEntry(id: number): Promise { await apiClient.delete(`/form16/26as/${id}`); } /** 26AS upload audit log entry (who uploaded, when, records imported). */ export interface Form1626asUploadHistoryItem { id: number; uploadedAt: string; uploadedBy: string; uploadedByEmail?: string | null; uploadedByDisplayName?: string | null; fileName?: string | null; recordsImported: number; errorsCount: number; } export async function get26asUploadHistory(limit?: number): Promise { const params = new URLSearchParams(); if (limit != null) params.set('limit', String(limit)); const query = params.toString(); const url = query ? `/form16/26as/upload-history?${query}` : '/form16/26as/upload-history'; const { data } = await apiClient.get<{ data?: { history?: Form1626asUploadHistoryItem[] }; history?: Form1626asUploadHistoryItem[] }>(url); const payload = data?.data ?? data; return payload?.history ?? []; } /** Timeout for 26AS upload (large files can take 1–2 min). */ const UPLOAD_26AS_TIMEOUT_MS = 5 * 60 * 1000; export interface Upload26asResult { imported: number; errors: string[]; } /** * Upload 26AS TXT file with optional progress callback (0–100%). * Uses XHR so we can report upload progress; after 100% the server may still be processing. */ export function upload26asFile( file: File, onProgress?: (percent: number) => void ): Promise { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); const url = `${API_BASE_URL}/form16/26as/upload`; const token = TokenManager.getAccessToken(); const timeoutId = setTimeout(() => { xhr.abort(); reject(new Error('Upload timed out. Try a smaller file or try again.')); }, UPLOAD_26AS_TIMEOUT_MS); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable && e.total > 0) { const percent = Math.min(100, Math.round((e.loaded / e.total) * 100)); onProgress?.(percent); } else { onProgress?.(0); } }); xhr.addEventListener('load', () => { clearTimeout(timeoutId); onProgress?.(100); const contentType = xhr.getResponseHeader('content-type'); const isJson = contentType?.includes('application/json'); const raw = xhr.responseText; let data: { data?: { imported?: number; errors?: string[] }; message?: string }; try { data = isJson ? JSON.parse(raw) : { message: raw || 'Request failed' }; } catch { data = { message: raw || 'Invalid response' }; } if (xhr.status >= 200 && xhr.status < 300) { const payload = (data.data ?? data) as { imported?: number; errors?: string[] } | undefined; resolve({ imported: payload?.imported ?? 0, errors: Array.isArray(payload?.errors) ? payload.errors : [], }); } else { reject(new Error((data as { message?: string }).message || `Request failed ${xhr.status}`)); } }); xhr.addEventListener('error', () => { clearTimeout(timeoutId); reject(new Error('Network error during upload')); }); xhr.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new Error('Upload was cancelled or timed out')); }); xhr.open('POST', url); if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.withCredentials = true; xhr.send(formData); }); } // ---------- RE: Non-submitted dealers ---------- export interface NotificationHistoryItem { date: string; notifiedBy: string; method: string; } export interface NonSubmittedDealerItem { id: string; dealerName: string; dealerCode: string; email: string; phone: string; location: string; missingQuarters: string[]; lastSubmissionDate: string | null; daysSinceLastSubmission: number | null; lastNotifiedDate: string | null; lastNotifiedBy: string | null; notificationCount: number; notificationHistory: NotificationHistoryItem[]; } export interface NonSubmittedDealersSummary { totalDealers: number; nonSubmittedCount: number; neverSubmittedCount: number; overdue90Count: number; } export interface ListNonSubmittedDealersResponse { summary: NonSubmittedDealersSummary; dealers: NonSubmittedDealerItem[]; } export async function listNonSubmittedDealers(financialYear?: string): Promise { const params = new URLSearchParams(); if (financialYear) params.set('financialYear', financialYear); const query = params.toString(); const url = query ? `/form16/non-submitted-dealers?${query}` : '/form16/non-submitted-dealers'; const { data } = await apiClient.get<{ data?: ListNonSubmittedDealersResponse }>(url); const payload = (data?.data ?? data) as ListNonSubmittedDealersResponse | undefined; const s = payload?.summary; return { summary: s ?? { totalDealers: 0, nonSubmittedCount: 0, neverSubmittedCount: 0, overdue90Count: 0, }, dealers: payload?.dealers ?? [], }; } /** Send "submit Form 16" notification to one non-submitted dealer. Returns updated dealer (with lastNotifiedDate set). */ export async function notifyNonSubmittedDealer(dealerCode: string, financialYear?: string): Promise { const { data } = await apiClient.post<{ data?: { dealer: NonSubmittedDealerItem } }>('/form16/non-submitted-dealers/notify', { dealerCode, financialYear: financialYear || undefined, }); const dealer = data?.data?.dealer; if (!dealer) throw new Error('No dealer returned'); return dealer; } // ---------- Dealer: Pending Submissions page ---------- export interface DealerPendingSubmissionItem { id: number; requestId: string; form16a_number: string; financial_year: string; quarter: string; version: number; version_number?: number; status: string; /** Display label: Completed | Resubmission needed | Duplicate submission | Failed | Under review (never Pending) */ display_status?: string; validation_status: string | null; submitted_date: string | null; total_amount?: number | null; credit_note_number?: string | null; document_url?: string | null; } export interface DealerPendingQuarterItem { financial_year: string; quarter: string; dealer_name?: string | null; has_submission: boolean; latest_submission_status: string | null; latest_submission_id: number | null; audit_start_date: string | null; twenty_six_as_start_date?: string | null; days_remaining: number | null; days_overdue: number | null; days_since_26as_uploaded?: number | null; } /** List dealer's Form 16 submissions (pending/failed). Dealer only. */ export async function listDealerSubmissions(params?: { status?: string; financialYear?: string; quarter?: string; }): Promise { const searchParams = new URLSearchParams(); if (params?.status) searchParams.set('status', params.status); if (params?.financialYear) searchParams.set('financialYear', params.financialYear); if (params?.quarter) searchParams.set('quarter', params.quarter); const query = searchParams.toString(); const url = query ? `/form16/dealer/submissions?${query}` : '/form16/dealer/submissions'; const { data } = await apiClient.get<{ data?: DealerPendingSubmissionItem[] }>(url); const payload = data?.data ?? data; return Array.isArray(payload) ? payload : []; } /** List dealer's pending quarters (no completed Form 16A). Dealer only. */ export async function listDealerPendingQuarters(): Promise { const { data } = await apiClient.get<{ data?: DealerPendingQuarterItem[] }>('/form16/dealer/pending-quarters'); const payload = data?.data ?? data; return Array.isArray(payload) ? payload : []; }