Re_Figma_Code/src/services/form16Api.ts

694 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, string>)['Content-Type'] = 'application/json';
}
if (token) {
(headers as Record<string, string>)['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<Form16Permissions> {
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<ListCreditNotesResponse> {
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<Form16CreditNoteItem | null> {
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<void> {
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<void> {
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<CreditNoteDetailResponse> {
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<SapCreditNoteResponse> {
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<SapDebitNoteResponse> {
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<ExtractOcrResponse> {
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<CreateForm16SubmissionResponse> {
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<Tds26asEntryItem>): 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<Tds26asEntryItem>): 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<void> {
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<Form1626asUploadHistoryItem[]> {
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 12 min). */
const UPLOAD_26AS_TIMEOUT_MS = 5 * 60 * 1000;
export interface Upload26asResult {
imported: number;
errors: string[];
}
/**
* Upload 26AS TXT file with optional progress callback (0100%).
* 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<Upload26asResult> {
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<ListNonSubmittedDealersResponse> {
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<NonSubmittedDealerItem> {
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<DealerPendingSubmissionItem[]> {
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<DealerPendingQuarterItem[]> {
const { data } = await apiClient.get<{ data?: DealerPendingQuarterItem[] }>('/form16/dealer/pending-quarters');
const payload = data?.data ?? data;
return Array.isArray(payload) ? payload : [];
}