multiple device login restricted and
in admin hsn sac code cofiguration added and csv file read approach changed to read at interval of 5 minutes with mutiple cred it note in single csv file
This commit is contained in:
parent
abba8aefdd
commit
f0435c47e4
@ -138,4 +138,4 @@ SAP_DISABLE_SSL_VERIFY=false
|
|||||||
# WFM Archive configuration examples (if overrides are needed)
|
# WFM Archive configuration examples (if overrides are needed)
|
||||||
# WFM_ARCHIVE_GST_CLAIMS_PATH=WFM-QRE\INCOMING\WFM_ARACHIVE\DLR_INC_CLAIMS_GST
|
# WFM_ARCHIVE_GST_CLAIMS_PATH=WFM-QRE\INCOMING\WFM_ARACHIVE\DLR_INC_CLAIMS_GST
|
||||||
# WFM_FORM16_CREDIT_ARCHIVE_PATH=WFM-QRE\INCOMING\WFM_ARACHIVE\FORM16_CRDT
|
# WFM_FORM16_CREDIT_ARCHIVE_PATH=WFM-QRE\INCOMING\WFM_ARACHIVE\FORM16_CRDT
|
||||||
|
#CREDIT_NOTE_SYNC_INTERVAL_MINUTES=1
|
||||||
|
|||||||
@ -23,7 +23,8 @@ export class AuthController {
|
|||||||
// Validate request body
|
// Validate request body
|
||||||
const validatedData = validateSSOCallback(req.body);
|
const validatedData = validateSSOCallback(req.body);
|
||||||
|
|
||||||
const result = await this.authService.handleSSOCallback(validatedData as any);
|
const userAgent = req.headers['user-agent'] || getRequestMetadata(req).userAgent;
|
||||||
|
const result = await this.authService.handleSSOCallback(validatedData as any, userAgent);
|
||||||
|
|
||||||
// Log login activity
|
// Log login activity
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
@ -180,7 +181,8 @@ export class AuthController {
|
|||||||
const { code, redirectUri } = validateTokenExchange(req.body);
|
const { code, redirectUri } = validateTokenExchange(req.body);
|
||||||
logger.info('Tanflow token exchange validation passed', { redirectUri });
|
logger.info('Tanflow token exchange validation passed', { redirectUri });
|
||||||
|
|
||||||
const result = await this.authService.exchangeTanflowCodeForTokens(code, redirectUri);
|
const userAgent = req.headers['user-agent'] || getRequestMetadata(req).userAgent;
|
||||||
|
const result = await this.authService.exchangeTanflowCodeForTokens(code, redirectUri, userAgent);
|
||||||
|
|
||||||
// Log login activity
|
// Log login activity
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
@ -395,6 +397,13 @@ export class AuthController {
|
|||||||
// Clear all cookies using multiple methods
|
// Clear all cookies using multiple methods
|
||||||
clearCookiesCompletely();
|
clearCookiesCompletely();
|
||||||
|
|
||||||
|
if (userId !== 'unknown') {
|
||||||
|
const user = await this.authService.getUserProfile(userId);
|
||||||
|
if (user) {
|
||||||
|
await this.authService.updateUserProfile(userId, { sessionToken: null, lastLoginDevice: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('User logout successful - cookies cleared', {
|
logger.info('User logout successful - cookies cleared', {
|
||||||
userId: req.user?.userId || 'unknown',
|
userId: req.user?.userId || 'unknown',
|
||||||
email: req.user?.email || 'unknown',
|
email: req.user?.email || 'unknown',
|
||||||
@ -452,7 +461,8 @@ export class AuthController {
|
|||||||
|
|
||||||
const { username, password } = validatePasswordLogin(req.body);
|
const { username, password } = validatePasswordLogin(req.body);
|
||||||
|
|
||||||
const result = await this.authService.authenticateWithPassword(username, password);
|
const userAgent = req.headers['user-agent'] || getRequestMetadata(req).userAgent;
|
||||||
|
const result = await this.authService.authenticateWithPassword(username, password, userAgent);
|
||||||
|
|
||||||
// Log login activity
|
// Log login activity
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
@ -535,7 +545,8 @@ export class AuthController {
|
|||||||
const { code, redirectUri } = validateTokenExchange(req.body);
|
const { code, redirectUri } = validateTokenExchange(req.body);
|
||||||
logger.info('Token exchange validation passed', { redirectUri });
|
logger.info('Token exchange validation passed', { redirectUri });
|
||||||
|
|
||||||
const result = await this.authService.exchangeCodeForTokens(code, redirectUri);
|
const userAgent = req.headers['user-agent'] || getRequestMetadata(req).userAgent;
|
||||||
|
const result = await this.authService.exchangeCodeForTokens(code, redirectUri, userAgent);
|
||||||
|
|
||||||
// Log login activity
|
// Log login activity
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { DealerClaimDetails } from '../models/DealerClaimDetails';
|
|||||||
import { ClaimInvoice } from '../models/ClaimInvoice';
|
import { ClaimInvoice } from '../models/ClaimInvoice';
|
||||||
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
||||||
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
||||||
|
import { ClaimCreditNoteItem } from '../models/ClaimCreditNoteItem';
|
||||||
import { ActivityType } from '../models/ActivityType';
|
import { ActivityType } from '../models/ActivityType';
|
||||||
import { Participant } from '../models/Participant';
|
import { Participant } from '../models/Participant';
|
||||||
import { sanitizeObject, sanitizePermissive } from '../utils/sanitizer';
|
import { sanitizeObject, sanitizePermissive } from '../utils/sanitizer';
|
||||||
@ -1230,10 +1231,29 @@ export class DealerClaimController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { wfmFileService } = await import('../services/wfmFile.service');
|
const { wfmFileService } = await import('../services/wfmFile.service');
|
||||||
const existingCreditNote = await ClaimCreditNote.findOne({ where: { requestId } });
|
const existingCreditNote = await ClaimCreditNote.findOne({
|
||||||
|
where: { requestId },
|
||||||
|
include: [{
|
||||||
|
model: ClaimCreditNoteItem,
|
||||||
|
as: 'items',
|
||||||
|
attributes: ['transactionNo'],
|
||||||
|
order: [['slNo', 'ASC']]
|
||||||
|
}]
|
||||||
|
}) as any;
|
||||||
|
|
||||||
if (existingCreditNote?.sapDocumentNumber || existingCreditNote?.creditNoteNumber) {
|
if (existingCreditNote?.sapDocumentNumber || existingCreditNote?.creditNoteNumber) {
|
||||||
|
let displayTxn = existingCreditNote.transactionNo || '';
|
||||||
|
const items = existingCreditNote.items || [];
|
||||||
|
if (items.length > 1) {
|
||||||
|
const first = items[0].transactionNo;
|
||||||
|
const last = items[items.length - 1].transactionNo;
|
||||||
|
if (first && last && first !== last) {
|
||||||
|
displayTxn = `${first} - ${last}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = [{
|
const payload = [{
|
||||||
TRNS_UNIQ_NO: '',
|
TRNS_UNIQ_NO: displayTxn,
|
||||||
CLAIM_NUMBER: requestNumber,
|
CLAIM_NUMBER: requestNumber,
|
||||||
DOC_NO: existingCreditNote.sapDocumentNumber || existingCreditNote.creditNoteNumber || '',
|
DOC_NO: existingCreditNote.sapDocumentNumber || existingCreditNote.creditNoteNumber || '',
|
||||||
MSG_TYP: existingCreditNote.status || '',
|
MSG_TYP: existingCreditNote.status || '',
|
||||||
@ -1247,26 +1267,31 @@ export class DealerClaimController {
|
|||||||
requestNumber,
|
requestNumber,
|
||||||
isNonGst
|
isNonGst
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!creditNoteData.length) {
|
if (!creditNoteData.length) {
|
||||||
return ResponseHandler.success(res, [], 'Credit note data fetched successfully');
|
return ResponseHandler.success(res, [], 'Credit note data fetched successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current requirement: process/store a single credit note per request.
|
// Process the file using the sync service (handles grouping and transactions)
|
||||||
const firstRow = creditNoteData[0] || {};
|
const { creditNoteSyncService } = await import('../services/creditNoteSync.service');
|
||||||
const existingAmount = existingCreditNote?.creditNoteAmount ?? 0;
|
await creditNoteSyncService.processFile(filePath);
|
||||||
await ClaimCreditNote.upsert({
|
|
||||||
requestId,
|
|
||||||
creditNoteNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || existingCreditNote?.creditNoteNumber || undefined,
|
|
||||||
sapDocumentNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || existingCreditNote?.sapDocumentNumber || undefined,
|
|
||||||
status: firstRow.MSG_TYP || existingCreditNote?.status || undefined,
|
|
||||||
errorMessage: firstRow.MESSAGE || existingCreditNote?.errorMessage || undefined,
|
|
||||||
creditNoteFilePath: filePath,
|
|
||||||
creditNoteAmount: Number(firstRow.CLAIM_AMT || firstRow.CREDIT_AMT || existingAmount || 0),
|
|
||||||
confirmedAt: new Date()
|
|
||||||
});
|
|
||||||
wfmFileService.deleteCreditNoteOutgoingFileByPath(filePath);
|
|
||||||
|
|
||||||
return ResponseHandler.success(res, [firstRow], 'Credit note data fetched successfully');
|
// Return unified row with range if multiple rows exist for this claim
|
||||||
|
const claimRows = creditNoteData.filter(row => row.CLAIM_NUMBER === requestNumber);
|
||||||
|
if (claimRows.length === 0) {
|
||||||
|
return ResponseHandler.success(res, [], 'Credit note data fetched successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimRow = { ...claimRows[0] };
|
||||||
|
if (claimRows.length > 1) {
|
||||||
|
const first = claimRows[0].TRNS_UNIQ_NO;
|
||||||
|
const last = claimRows[claimRows.length - 1].TRNS_UNIQ_NO;
|
||||||
|
if (first && last && first !== last) {
|
||||||
|
claimRow.TRNS_UNIQ_NO = `${first} - ${last}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHandler.success(res, [claimRow], 'Credit note data fetched successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
logger.error('[DealerClaimController] Error fetching credit note WFM data:', error);
|
logger.error('[DealerClaimController] Error fetching credit note WFM data:', error);
|
||||||
|
|||||||
133
src/controllers/hsnSacCode.controller.ts
Normal file
133
src/controllers/hsnSacCode.controller.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { hsnSacCodeService } from '../services/hsnSacCode.service';
|
||||||
|
import { ResponseHandler } from '../utils/responseHandler';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export class HsnSacCodeController {
|
||||||
|
/**
|
||||||
|
* Get HSN/SAC codes with pagination and search
|
||||||
|
*/
|
||||||
|
async getAllCodes(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const onlyActive = req.query.active === 'true';
|
||||||
|
const page = parseInt(req.query.page as string) || 1;
|
||||||
|
const limit = parseInt(req.query.limit as string) || 10;
|
||||||
|
const search = req.query.search as string;
|
||||||
|
|
||||||
|
const result = await hsnSacCodeService.getAllCodes(onlyActive, page, limit, search);
|
||||||
|
|
||||||
|
ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
result.codes,
|
||||||
|
'HSN/SAC codes fetched successfully',
|
||||||
|
200,
|
||||||
|
result.pagination
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in getAllCodes controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to fetch HSN/SAC codes', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get code by ID
|
||||||
|
*/
|
||||||
|
async getCodeById(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const code = await hsnSacCodeService.getCodeById(id);
|
||||||
|
if (!code) {
|
||||||
|
return ResponseHandler.error(res, 'HSN/SAC code not found', 404);
|
||||||
|
}
|
||||||
|
ResponseHandler.success(res, code, 'HSN/SAC code fetched successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in getCodeById controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to fetch HSN/SAC code', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new code
|
||||||
|
*/
|
||||||
|
async createCode(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { code, type, gstRate, description, isActive } = req.body;
|
||||||
|
|
||||||
|
if (!code || !type) {
|
||||||
|
return ResponseHandler.error(res, 'Code and type are required', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCode = await hsnSacCodeService.createCode({
|
||||||
|
code,
|
||||||
|
type,
|
||||||
|
gstRate,
|
||||||
|
description,
|
||||||
|
isActive: isActive !== undefined ? isActive : true
|
||||||
|
});
|
||||||
|
|
||||||
|
ResponseHandler.success(res, newCode, 'HSN/SAC code created successfully', 201);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in createCode controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to create HSN/SAC code', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update code
|
||||||
|
*/
|
||||||
|
async updateCode(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const updatedCode = await hsnSacCodeService.updateCode(id, req.body);
|
||||||
|
|
||||||
|
if (!updatedCode) {
|
||||||
|
return ResponseHandler.error(res, 'HSN/SAC code not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHandler.success(res, updatedCode, 'HSN/SAC code updated successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in updateCode controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to update HSN/SAC code', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete code
|
||||||
|
*/
|
||||||
|
async deleteCode(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const success = await hsnSacCodeService.deleteCode(id);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return ResponseHandler.error(res, 'HSN/SAC code not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHandler.success(res, null, 'HSN/SAC code deleted successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in deleteCode controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to delete HSN/SAC code', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle active status
|
||||||
|
*/
|
||||||
|
async toggleActive(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const updatedCode = await hsnSacCodeService.toggleActive(id);
|
||||||
|
|
||||||
|
if (!updatedCode) {
|
||||||
|
return ResponseHandler.error(res, 'HSN/SAC code not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseHandler.success(res, updatedCode, 'HSN/SAC code status toggled successfully');
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error in toggleActive controller:', error);
|
||||||
|
ResponseHandler.error(res, 'Failed to toggle HSN/SAC code status', 500, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsnSacCodeController = new HsnSacCodeController();
|
||||||
25
src/jobs/creditNoteSyncJob.ts
Normal file
25
src/jobs/creditNoteSyncJob.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { creditNoteSyncService } from '../services/creditNoteSync.service';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main sync function to process all outgoing files
|
||||||
|
* Delegates to creditNoteSyncService
|
||||||
|
*/
|
||||||
|
export async function syncCreditNotes(): Promise<void> {
|
||||||
|
await creditNoteSyncService.syncCreditNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the background sync job
|
||||||
|
*/
|
||||||
|
export function startCreditNoteSyncJob(): void {
|
||||||
|
const intervalMinutes = Number(process.env.CREDIT_NOTE_SYNC_INTERVAL_MINUTES) || 5;
|
||||||
|
logger.info(`[CreditNoteSyncJob] Background job initialized (Interval: ${intervalMinutes}m)`);
|
||||||
|
|
||||||
|
// Run once immediately on startup
|
||||||
|
syncCreditNotes().catch(err => logger.error('[CreditNoteSyncJob] Initial sync failed:', err));
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
syncCreditNotes().catch(err => logger.error('[CreditNoteSyncJob] Periodic sync failed:', err));
|
||||||
|
}, intervalMinutes * 60 * 1000);
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ interface JwtPayload {
|
|||||||
employeeId: string;
|
employeeId: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
sessionToken: string;
|
||||||
iat: number;
|
iat: number;
|
||||||
exp: number;
|
exp: number;
|
||||||
}
|
}
|
||||||
@ -70,6 +71,15 @@ export const authenticateToken = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!decoded.sessionToken || decoded.sessionToken !== user.sessionToken) {
|
||||||
|
res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
errorCode: 'SESSION_SUPERSEDED',
|
||||||
|
message: `You have been logged out because an active session was detected from ${user.lastLoginDevice || 'another device'}.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Attach user info to request object
|
// Attach user info to request object
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
@ -117,6 +127,7 @@ export const optionalAuth = async (
|
|||||||
const user = await User.findByPk(decoded.userId);
|
const user = await User.findByPk(decoded.userId);
|
||||||
|
|
||||||
if (user && user.isActive) {
|
if (user && user.isActive) {
|
||||||
|
if (!decoded.sessionToken || decoded.sessionToken === user.sessionToken) {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@ -125,6 +136,7 @@ export const optionalAuth = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { QueryInterface, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface: QueryInterface) => {
|
||||||
|
// 1. Add sessionToken to users table
|
||||||
|
await queryInterface.addColumn('users', 'sessionToken', {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Unique token for active session to restrict concurrent logins'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Add lastLoginDevice to users table
|
||||||
|
await queryInterface.addColumn('users', 'lastLoginDevice', {
|
||||||
|
type: DataTypes.STRING(255),
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Browser/Device string from User-Agent of the active session'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create hsn_sac_codes table
|
||||||
|
await queryInterface.createTable('hsn_sac_codes', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'The HSN or SAC code value'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: DataTypes.ENUM('HSN', 'SAC'),
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Type of code: either HSN or SAC'
|
||||||
|
},
|
||||||
|
gstRate: {
|
||||||
|
type: DataTypes.DECIMAL(5, 2),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'gst_rate',
|
||||||
|
comment: 'Associated GST rate percentage'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Description of the code'
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'is_active'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'updated_at'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add indexes to hsn_sac_codes
|
||||||
|
await queryInterface.addIndex('hsn_sac_codes', ['code']);
|
||||||
|
await queryInterface.addIndex('hsn_sac_codes', ['type']);
|
||||||
|
await queryInterface.addIndex('hsn_sac_codes', ['is_active']);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface: QueryInterface) => {
|
||||||
|
// Drop hsn_sac_codes table
|
||||||
|
await queryInterface.dropTable('hsn_sac_codes');
|
||||||
|
|
||||||
|
// Remove columns from users table
|
||||||
|
await queryInterface.removeColumn('users', 'lastLoginDevice');
|
||||||
|
await queryInterface.removeColumn('users', 'sessionToken');
|
||||||
|
|
||||||
|
// Also drop the ENUM type created for hsn_sac_codes type
|
||||||
|
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_hsn_sac_codes_type";');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,166 @@
|
|||||||
|
import { QueryInterface, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface: QueryInterface) => {
|
||||||
|
// 1. Update claim_credit_notes table with idempotency checks
|
||||||
|
const tableDefinition = await queryInterface.describeTable('claim_credit_notes');
|
||||||
|
|
||||||
|
if (!tableDefinition.transaction_no) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'transaction_no', {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Overall PWC transaction unique number'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tableDefinition.tds_amount) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'tds_amount', {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
comment: 'TDS amount for the credit note'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tableDefinition.credit_amount) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'credit_amount', {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
comment: 'Final credit amount after TDS'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop redundant columns if they exist
|
||||||
|
const columnsToDrop = [
|
||||||
|
'gst_rate', 'gst_amt', 'cgst_rate', 'cgst_amt',
|
||||||
|
'sgst_rate', 'sgst_amt', 'igst_rate', 'igst_amt',
|
||||||
|
'utgst_rate', 'utgst_amt', 'cess_rate', 'cess_amt',
|
||||||
|
'total_amt'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const column of columnsToDrop) {
|
||||||
|
if (tableDefinition[column]) {
|
||||||
|
await queryInterface.removeColumn('claim_credit_notes', column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create claim_credit_note_items table (Refined & Unified)
|
||||||
|
const allTables = await queryInterface.showAllTables();
|
||||||
|
const tableExists = allTables.some(t => {
|
||||||
|
const name = typeof t === 'string' ? t : (t as any).tableName;
|
||||||
|
return name.toLowerCase() === 'claim_credit_note_items';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tableExists) {
|
||||||
|
await queryInterface.createTable('claim_credit_note_items', {
|
||||||
|
item_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
credit_note_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'claim_credit_notes',
|
||||||
|
key: 'credit_note_id',
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
},
|
||||||
|
sl_no: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
transaction_no: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Per-item TRNS_UNIQ_NO'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
hsn_cd: {
|
||||||
|
type: DataTypes.STRING(20),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
claim_amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
tds_amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
credit_amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add index for performance
|
||||||
|
await queryInterface.addIndex('claim_credit_note_items', ['credit_note_id']);
|
||||||
|
await queryInterface.addIndex('claim_credit_note_items', ['transaction_no']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface: QueryInterface) => {
|
||||||
|
// Drop the items table first
|
||||||
|
await queryInterface.dropTable('claim_credit_note_items');
|
||||||
|
|
||||||
|
// Re-add dropped columns to claim_credit_notes (if they were removed)
|
||||||
|
const tableDefinition = await queryInterface.describeTable('claim_credit_notes');
|
||||||
|
const columnsToReAdd = [
|
||||||
|
{ name: 'gst_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'gst_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'cgst_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'cgst_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'sgst_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'sgst_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'igst_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'igst_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'utgst_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'utgst_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'cess_rate', type: DataTypes.DECIMAL(5, 2) },
|
||||||
|
{ name: 'cess_amt', type: DataTypes.DECIMAL(15, 2) },
|
||||||
|
{ name: 'total_amt', type: DataTypes.DECIMAL(15, 2) }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const col of columnsToReAdd) {
|
||||||
|
if (!tableDefinition[col.name]) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', col.name, {
|
||||||
|
type: col.type,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove added columns
|
||||||
|
const addedCols = ['credit_amount', 'tds_amount', 'transaction_no'];
|
||||||
|
for (const col of addedCols) {
|
||||||
|
if (tableDefinition[col]) {
|
||||||
|
await queryInterface.removeColumn('claim_credit_notes', col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -10,19 +10,9 @@ interface ClaimCreditNoteAttributes {
|
|||||||
creditNoteNumber?: string;
|
creditNoteNumber?: string;
|
||||||
creditNoteDate?: Date;
|
creditNoteDate?: Date;
|
||||||
creditNoteAmount: number;
|
creditNoteAmount: number;
|
||||||
gstRate?: number;
|
transactionNo?: string;
|
||||||
gstAmt?: number;
|
tdsAmount?: number;
|
||||||
cgstRate?: number;
|
creditAmount?: number;
|
||||||
cgstAmt?: number;
|
|
||||||
sgstRate?: number;
|
|
||||||
sgstAmt?: number;
|
|
||||||
igstRate?: number;
|
|
||||||
igstAmt?: number;
|
|
||||||
utgstRate?: number;
|
|
||||||
utgstAmt?: number;
|
|
||||||
cessRate?: number;
|
|
||||||
cessAmt?: number;
|
|
||||||
totalAmt?: number;
|
|
||||||
sapDocumentNumber?: string;
|
sapDocumentNumber?: string;
|
||||||
creditNoteFilePath?: string;
|
creditNoteFilePath?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@ -35,7 +25,7 @@ interface ClaimCreditNoteAttributes {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaimCreditNoteCreationAttributes extends Optional<ClaimCreditNoteAttributes, 'creditNoteId' | 'invoiceId' | 'creditNoteNumber' | 'creditNoteDate' | 'gstRate' | 'gstAmt' | 'cgstRate' | 'cgstAmt' | 'sgstRate' | 'sgstAmt' | 'igstRate' | 'igstAmt' | 'utgstRate' | 'utgstAmt' | 'cessRate' | 'cessAmt' | 'totalAmt' | 'sapDocumentNumber' | 'creditNoteFilePath' | 'status' | 'errorMessage' | 'confirmedBy' | 'confirmedAt' | 'reason' | 'description' | 'createdAt' | 'updatedAt'> { }
|
interface ClaimCreditNoteCreationAttributes extends Optional<ClaimCreditNoteAttributes, 'creditNoteId' | 'invoiceId' | 'creditNoteNumber' | 'creditNoteDate' | 'transactionNo' | 'tdsAmount' | 'creditAmount' | 'sapDocumentNumber' | 'creditNoteFilePath' | 'status' | 'errorMessage' | 'confirmedBy' | 'confirmedAt' | 'reason' | 'description' | 'createdAt' | 'updatedAt'> { }
|
||||||
|
|
||||||
class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCreationAttributes> implements ClaimCreditNoteAttributes {
|
class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCreationAttributes> implements ClaimCreditNoteAttributes {
|
||||||
public creditNoteId!: string;
|
public creditNoteId!: string;
|
||||||
@ -44,19 +34,9 @@ class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCr
|
|||||||
public creditNoteNumber?: string;
|
public creditNoteNumber?: string;
|
||||||
public creditNoteDate?: Date;
|
public creditNoteDate?: Date;
|
||||||
public creditNoteAmount!: number;
|
public creditNoteAmount!: number;
|
||||||
public gstRate?: number;
|
public transactionNo?: string;
|
||||||
public gstAmt?: number;
|
public tdsAmount?: number;
|
||||||
public cgstRate?: number;
|
public creditAmount?: number;
|
||||||
public cgstAmt?: number;
|
|
||||||
public sgstRate?: number;
|
|
||||||
public sgstAmt?: number;
|
|
||||||
public igstRate?: number;
|
|
||||||
public igstAmt?: number;
|
|
||||||
public utgstRate?: number;
|
|
||||||
public utgstAmt?: number;
|
|
||||||
public cessRate?: number;
|
|
||||||
public cessAmt?: number;
|
|
||||||
public totalAmt?: number;
|
|
||||||
public sapDocumentNumber?: string;
|
public sapDocumentNumber?: string;
|
||||||
public creditNoteFilePath?: string;
|
public creditNoteFilePath?: string;
|
||||||
public status?: string;
|
public status?: string;
|
||||||
@ -115,70 +95,22 @@ ClaimCreditNote.init(
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
field: 'credit_amount'
|
field: 'credit_amount'
|
||||||
},
|
},
|
||||||
gstRate: {
|
transactionNo: {
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'gst_rate'
|
field: 'transaction_no',
|
||||||
},
|
},
|
||||||
gstAmt: {
|
tdsAmount: {
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'gst_amt'
|
defaultValue: 0,
|
||||||
|
field: 'tds_amount',
|
||||||
},
|
},
|
||||||
cgstRate: {
|
creditAmount: {
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'cgst_rate'
|
|
||||||
},
|
|
||||||
cgstAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'cgst_amt'
|
defaultValue: 0,
|
||||||
},
|
field: 'credit_amount',
|
||||||
sgstRate: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'sgst_rate'
|
|
||||||
},
|
|
||||||
sgstAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'sgst_amt'
|
|
||||||
},
|
|
||||||
igstRate: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'igst_rate'
|
|
||||||
},
|
|
||||||
igstAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'igst_amt'
|
|
||||||
},
|
|
||||||
utgstRate: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'utgst_rate'
|
|
||||||
},
|
|
||||||
utgstAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'utgst_amt'
|
|
||||||
},
|
|
||||||
cessRate: {
|
|
||||||
type: DataTypes.DECIMAL(5, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'cess_rate'
|
|
||||||
},
|
|
||||||
cessAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'cess_amt'
|
|
||||||
},
|
|
||||||
totalAmt: {
|
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'total_amt'
|
|
||||||
},
|
},
|
||||||
sapDocumentNumber: {
|
sapDocumentNumber: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
|
|||||||
83
src/models/ClaimCreditNoteItem.ts
Normal file
83
src/models/ClaimCreditNoteItem.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '@config/database';
|
||||||
|
|
||||||
|
export class ClaimCreditNoteItem extends Model {
|
||||||
|
public itemId!: string;
|
||||||
|
public creditNoteId!: string;
|
||||||
|
public slNo!: number;
|
||||||
|
public transactionNo!: string | null;
|
||||||
|
public description!: string | null;
|
||||||
|
public hsnCd!: string | null;
|
||||||
|
public amount!: number;
|
||||||
|
public claimAmount!: number | null;
|
||||||
|
public tdsAmount!: number | null;
|
||||||
|
public creditAmount!: number | null;
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
public readonly updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClaimCreditNoteItem.init(
|
||||||
|
{
|
||||||
|
itemId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
field: 'item_id',
|
||||||
|
},
|
||||||
|
creditNoteId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'credit_note_id',
|
||||||
|
},
|
||||||
|
slNo: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'sl_no',
|
||||||
|
},
|
||||||
|
transactionNo: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'transaction_no',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'description',
|
||||||
|
},
|
||||||
|
hsnCd: {
|
||||||
|
type: DataTypes.STRING(20),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'hsn_cd',
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'amount',
|
||||||
|
},
|
||||||
|
claimAmount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'claim_amount',
|
||||||
|
},
|
||||||
|
tdsAmount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'tds_amount',
|
||||||
|
},
|
||||||
|
creditAmount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'credit_amount',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
tableName: 'claim_credit_note_items',
|
||||||
|
underscored: true,
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
95
src/models/HsnSacCode.ts
Normal file
95
src/models/HsnSacCode.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
import { sequelize } from '../config/database';
|
||||||
|
|
||||||
|
export type CodeType = 'HSN' | 'SAC';
|
||||||
|
|
||||||
|
export interface HsnSacCodeAttributes {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
type: CodeType;
|
||||||
|
gstRate?: number | null;
|
||||||
|
description?: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HsnSacCodeCreationAttributes extends Optional<HsnSacCodeAttributes, 'id' | 'gstRate' | 'description' | 'isActive' | 'createdAt' | 'updatedAt'> { }
|
||||||
|
|
||||||
|
class HsnSacCode extends Model<HsnSacCodeAttributes, HsnSacCodeCreationAttributes> implements HsnSacCodeAttributes {
|
||||||
|
public id!: string;
|
||||||
|
public code!: string;
|
||||||
|
public type!: CodeType;
|
||||||
|
public gstRate?: number | null;
|
||||||
|
public description?: string | null;
|
||||||
|
public isActive!: boolean;
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
public readonly updatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
HsnSacCode.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'The HSN or SAC code value'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: DataTypes.ENUM('HSN', 'SAC'),
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Type of code: either HSN or SAC'
|
||||||
|
},
|
||||||
|
gstRate: {
|
||||||
|
type: DataTypes.DECIMAL(5, 2),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'gst_rate',
|
||||||
|
comment: 'Associated GST rate percentage'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Description of the code'
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'is_active'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'updated_at'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'HsnSacCode',
|
||||||
|
tableName: 'hsn_sac_codes',
|
||||||
|
timestamps: true,
|
||||||
|
createdAt: 'created_at',
|
||||||
|
updatedAt: 'updated_at',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['code']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['type']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['is_active']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export { HsnSacCode };
|
||||||
@ -51,11 +51,13 @@ interface UserAttributes {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
role: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
role: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
||||||
lastLogin?: Date;
|
lastLogin?: Date;
|
||||||
|
sessionToken?: string | null;
|
||||||
|
lastLoginDevice?: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserCreationAttributes extends Optional<UserAttributes, 'userId' | 'employeeId' | 'department' | 'designation' | 'phone' | 'manager' | 'secondEmail' | 'jobTitle' | 'employeeNumber' | 'postalAddress' | 'mobilePhone' | 'adGroups' | 'emailNotificationsEnabled' | 'pushNotificationsEnabled' | 'inAppNotificationsEnabled' | 'role' | 'lastLogin' | 'createdAt' | 'updatedAt'> { }
|
interface UserCreationAttributes extends Optional<UserAttributes, 'userId' | 'employeeId' | 'department' | 'designation' | 'phone' | 'manager' | 'secondEmail' | 'jobTitle' | 'employeeNumber' | 'postalAddress' | 'mobilePhone' | 'adGroups' | 'emailNotificationsEnabled' | 'pushNotificationsEnabled' | 'inAppNotificationsEnabled' | 'role' | 'lastLogin' | 'sessionToken' | 'lastLoginDevice' | 'createdAt' | 'updatedAt'> { }
|
||||||
|
|
||||||
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||||
public userId!: string;
|
public userId!: string;
|
||||||
@ -95,6 +97,8 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
|
|||||||
public isActive!: boolean;
|
public isActive!: boolean;
|
||||||
public role!: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
public role!: UserRole; // RBAC: USER, MANAGEMENT, ADMIN
|
||||||
public lastLogin?: Date;
|
public lastLogin?: Date;
|
||||||
|
public sessionToken?: string | null;
|
||||||
|
public lastLoginDevice?: string | null;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
public updatedAt!: Date;
|
public updatedAt!: Date;
|
||||||
|
|
||||||
@ -278,6 +282,18 @@ User.init(
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'last_login'
|
field: 'last_login'
|
||||||
},
|
},
|
||||||
|
sessionToken: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'sessionToken',
|
||||||
|
comment: 'Unique token for active session to restrict concurrent logins'
|
||||||
|
},
|
||||||
|
lastLoginDevice: {
|
||||||
|
type: DataTypes.STRING(255),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'lastLoginDevice',
|
||||||
|
comment: 'Browser/Device string from User-Agent of the active session'
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { WorkflowTemplate } from './WorkflowTemplate';
|
|||||||
import { ClaimInvoice } from './ClaimInvoice';
|
import { ClaimInvoice } from './ClaimInvoice';
|
||||||
import { ClaimInvoiceItem } from './ClaimInvoiceItem';
|
import { ClaimInvoiceItem } from './ClaimInvoiceItem';
|
||||||
import { ClaimCreditNote } from './ClaimCreditNote';
|
import { ClaimCreditNote } from './ClaimCreditNote';
|
||||||
|
import { ClaimCreditNoteItem } from './ClaimCreditNoteItem';
|
||||||
import { Form16aSubmission } from './Form16aSubmission';
|
import { Form16aSubmission } from './Form16aSubmission';
|
||||||
import { Form16CreditNote } from './Form16CreditNote';
|
import { Form16CreditNote } from './Form16CreditNote';
|
||||||
import { Form16DebitNote } from './Form16DebitNote';
|
import { Form16DebitNote } from './Form16DebitNote';
|
||||||
@ -178,6 +179,13 @@ const defineAssociations = () => {
|
|||||||
sourceKey: 'id'
|
sourceKey: 'id'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ClaimCreditNote associations
|
||||||
|
ClaimCreditNote.hasMany(ClaimCreditNoteItem, {
|
||||||
|
as: 'items',
|
||||||
|
foreignKey: 'creditNoteId',
|
||||||
|
sourceKey: 'creditNoteId'
|
||||||
|
});
|
||||||
|
|
||||||
// Note: belongsTo associations are defined in individual model files to avoid duplicate alias conflicts
|
// Note: belongsTo associations are defined in individual model files to avoid duplicate alias conflicts
|
||||||
// Only hasMany associations from WorkflowRequest are defined here since they're one-way
|
// Only hasMany associations from WorkflowRequest are defined here since they're one-way
|
||||||
};
|
};
|
||||||
@ -216,6 +224,7 @@ export {
|
|||||||
ClaimInvoice,
|
ClaimInvoice,
|
||||||
ClaimInvoiceItem,
|
ClaimInvoiceItem,
|
||||||
ClaimCreditNote,
|
ClaimCreditNote,
|
||||||
|
ClaimCreditNoteItem,
|
||||||
Form16aSubmission,
|
Form16aSubmission,
|
||||||
Form16CreditNote,
|
Form16CreditNote,
|
||||||
Form16DebitNote,
|
Form16DebitNote,
|
||||||
|
|||||||
56
src/routes/hsnSacCode.routes.ts
Normal file
56
src/routes/hsnSacCode.routes.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { hsnSacCodeController } from '../controllers/hsnSacCode.controller';
|
||||||
|
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||||
|
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// All HSN/SAC routes require authentication
|
||||||
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/v1/hsn-sac
|
||||||
|
* @desc Get all HSN/SAC codes
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => hsnSacCodeController.getAllCodes(req, res));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/v1/hsn-sac/:id
|
||||||
|
* @desc Get code by ID
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/:id', (req, res) => hsnSacCodeController.getCodeById(req, res));
|
||||||
|
|
||||||
|
// Admin only routes for modification
|
||||||
|
router.use(requireAdmin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/v1/hsn-sac
|
||||||
|
* @desc Create new HSN/SAC code
|
||||||
|
* @access Private/Admin
|
||||||
|
*/
|
||||||
|
router.post('/', (req, res) => hsnSacCodeController.createCode(req, res));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/v1/hsn-sac/:id
|
||||||
|
* @desc Update HSN/SAC code
|
||||||
|
* @access Private/Admin
|
||||||
|
*/
|
||||||
|
router.patch('/:id', (req, res) => hsnSacCodeController.updateCode(req, res));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/v1/hsn-sac/:id
|
||||||
|
* @desc Delete HSN/SAC code
|
||||||
|
* @access Private/Admin
|
||||||
|
*/
|
||||||
|
router.delete('/:id', (req, res) => hsnSacCodeController.deleteCode(req, res));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/v1/hsn-sac/:id/toggle-active
|
||||||
|
* @desc Toggle active status
|
||||||
|
* @access Private/Admin
|
||||||
|
*/
|
||||||
|
router.patch('/:id/toggle-active', (req, res) => hsnSacCodeController.toggleActive(req, res));
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -34,6 +34,7 @@ import apiTokenRoutes from './apiToken.routes';
|
|||||||
import antivirusRoutes from './antivirus.routes';
|
import antivirusRoutes from './antivirus.routes';
|
||||||
import dealerExternalRoutes from './dealerExternal.routes';
|
import dealerExternalRoutes from './dealerExternal.routes';
|
||||||
import form16Routes from './form16.routes';
|
import form16Routes from './form16.routes';
|
||||||
|
import hsnSacCodeRoutes from './hsnSacCode.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -99,6 +100,7 @@ router.use('/dealers', generalApiLimiter, dealerRoutes); // 200 r
|
|||||||
router.use('/dealers-external', generalApiLimiter, dealerExternalRoutes); // 200 req/15min
|
router.use('/dealers-external', generalApiLimiter, dealerExternalRoutes); // 200 req/15min
|
||||||
router.use('/form16', uploadLimiter, form16Routes); // 50 req/15min (file uploads: extract, submissions, 26as)
|
router.use('/form16', uploadLimiter, form16Routes); // 50 req/15min (file uploads: extract, submissions, 26as)
|
||||||
router.use('/api-tokens', authLimiter, apiTokenRoutes); // 20 req/15min (sensitive — same as auth)
|
router.use('/api-tokens', authLimiter, apiTokenRoutes); // 20 req/15min (sensitive — same as auth)
|
||||||
|
router.use('/hsn-sac', generalApiLimiter, hsnSacCodeRoutes); // 200 req/15min
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
@ -180,6 +180,8 @@ async function runMigrations(): Promise<void> {
|
|||||||
const m63 = require('../migrations/20260317120001-add-form16-trns-uniq-no');
|
const m63 = require('../migrations/20260317120001-add-form16-trns-uniq-no');
|
||||||
const m64 = require('../migrations/20260318100001-create-form16-debit-note-sap-responses');
|
const m64 = require('../migrations/20260318100001-create-form16-debit-note-sap-responses');
|
||||||
const m65 = require('../migrations/20260318200001-add-sap-response-csv-fields');
|
const m65 = require('../migrations/20260318200001-add-sap-response-csv-fields');
|
||||||
|
const m66 = require('../migrations/20260325094500-add-user-session-and-hsn-sac-codes');
|
||||||
|
const m67 = require('../migrations/20260325175000-update-credit-notes-and-add-items');
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
@ -252,6 +254,8 @@ async function runMigrations(): Promise<void> {
|
|||||||
{ name: '20260317120001-add-form16-trns-uniq-no', module: m63 },
|
{ name: '20260317120001-add-form16-trns-uniq-no', module: m63 },
|
||||||
{ name: '20260318100001-create-form16-debit-note-sap-responses', module: m64 },
|
{ name: '20260318100001-create-form16-debit-note-sap-responses', module: m64 },
|
||||||
{ name: '20260318200001-add-sap-response-csv-fields', module: m65 },
|
{ name: '20260318200001-add-sap-response-csv-fields', module: m65 },
|
||||||
|
{ name: '20260325094500-add-user-session-and-hsn-sac-codes', module: m66 },
|
||||||
|
{ name: '20260325175000-update-credit-notes-and-add-items', module: m67 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Dynamically import sequelize after secrets are loaded
|
// Dynamically import sequelize after secrets are loaded
|
||||||
|
|||||||
@ -70,6 +70,7 @@ import * as m62 from '../migrations/20260317100001-create-form16-sap-responses';
|
|||||||
import * as m63 from '../migrations/20260317120001-add-form16-trns-uniq-no';
|
import * as m63 from '../migrations/20260317120001-add-form16-trns-uniq-no';
|
||||||
import * as m64 from '../migrations/20260318100001-create-form16-debit-note-sap-responses';
|
import * as m64 from '../migrations/20260318100001-create-form16-debit-note-sap-responses';
|
||||||
import * as m65 from '../migrations/20260318200001-add-sap-response-csv-fields';
|
import * as m65 from '../migrations/20260318200001-add-sap-response-csv-fields';
|
||||||
|
import * as m66 from '../migrations/20260325094500-add-user-session-and-hsn-sac-codes';
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -147,7 +148,7 @@ const migrations: Migration[] = [
|
|||||||
{ name: '20260317120001-add-form16-trns-uniq-no', module: m63 },
|
{ name: '20260317120001-add-form16-trns-uniq-no', module: m63 },
|
||||||
{ name: '20260318100001-create-form16-debit-note-sap-responses', module: m64 },
|
{ name: '20260318100001-create-form16-debit-note-sap-responses', module: m64 },
|
||||||
{ name: '20260318200001-add-sap-response-csv-fields', module: m65 },
|
{ name: '20260318200001-add-sap-response-csv-fields', module: m65 },
|
||||||
|
{ name: '20260325094500-add-user-session-and-hsn-sac-codes', module: m66 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -119,6 +119,8 @@ const startServer = async (): Promise<void> => {
|
|||||||
startForm16NotificationJobs();
|
startForm16NotificationJobs();
|
||||||
const { startForm16ArchiveJob } = require('./services/form16Archive.service');
|
const { startForm16ArchiveJob } = require('./services/form16Archive.service');
|
||||||
startForm16ArchiveJob();
|
startForm16ArchiveJob();
|
||||||
|
const { startCreditNoteSyncJob } = require('./jobs/creditNoteSyncJob');
|
||||||
|
startCreditNoteSyncJob();
|
||||||
|
|
||||||
// Initialize queue metrics collection for Prometheus
|
// Initialize queue metrics collection for Prometheus
|
||||||
initializeQueueMetrics();
|
initializeQueueMetrics();
|
||||||
|
|||||||
@ -9,19 +9,19 @@ import { ActivityType } from '@models/ActivityType';
|
|||||||
*/
|
*/
|
||||||
const DEFAULT_ACTIVITY_TYPES = [
|
const DEFAULT_ACTIVITY_TYPES = [
|
||||||
{ title: 'Riders Mania Claims', itemCode: '1', taxationType: 'Non GST', sapRefNo: 'ZRDM', creditPostingOn: 'Spares' },
|
{ title: 'Riders Mania Claims', itemCode: '1', taxationType: 'Non GST', sapRefNo: 'ZRDM', creditPostingOn: 'Spares' },
|
||||||
{ title: 'Marketing Cost – Bike to Vendor', itemCode: '2', taxationType: 'Non GST', sapRefNo: 'ZMBV', creditPostingOn: 'Vehicle' },
|
{ title: 'Marketing Cost – Bike to Vendor', itemCode: '2', taxationType: 'Non GST', sapRefNo: 'ZMBV', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Media Bike Service', itemCode: '3', taxationType: 'GST', sapRefNo: 'ZMBS', creditPostingOn: 'Spares' },
|
{ title: 'Media Bike Service', itemCode: '3', taxationType: 'GST', sapRefNo: 'ZMBS', creditPostingOn: 'Spares' },
|
||||||
{ title: 'ARAI Motorcycle Liquidation', itemCode: '4', taxationType: 'GST', sapRefNo: 'ZAML', creditPostingOn: 'Vehicle' },
|
{ title: 'ARAI Motorcycle Liquidation', itemCode: '4', taxationType: 'GST', sapRefNo: 'ZAML', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'ARAI Certification – STA Approval CNR', itemCode: '5', taxationType: 'Non GST', sapRefNo: 'ZACS', creditPostingOn: 'Vehicle' },
|
{ title: 'ARAI Certification – STA Approval CNR', itemCode: '5', taxationType: 'Non GST', sapRefNo: 'ZACS', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Procurement of Spares/Apparel/GMA for Events', itemCode: '6', taxationType: 'GST', sapRefNo: 'ZPPE', creditPostingOn: 'Spares' },
|
{ title: 'Procurement of Spares/Apparel/GMA for Events', itemCode: '6', taxationType: 'GST', sapRefNo: 'ZPPE', creditPostingOn: 'Spares' },
|
||||||
{ title: 'Fuel for Media Bike Used for Event', itemCode: '7', taxationType: 'Non GST', sapRefNo: 'ZFMB', creditPostingOn: 'Vehicle' },
|
{ title: 'Fuel for Media Bike Used for Event', itemCode: '7', taxationType: 'Non GST', sapRefNo: 'ZFMB', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Motorcycle Buyback and Goodwill Support', itemCode: '8', taxationType: 'Non GST', sapRefNo: 'ZMBG', creditPostingOn: 'Vehicle' },
|
{ title: 'Motorcycle Buyback and Goodwill Support', itemCode: '8', taxationType: 'Non GST', sapRefNo: 'ZMBG', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Liquidation of Used Motorcycle', itemCode: '9', taxationType: 'GST', sapRefNo: 'ZLUM', creditPostingOn: 'Vehicle' },
|
{ title: 'Liquidation of Used Motorcycle', itemCode: '9', taxationType: 'GST', sapRefNo: 'ZLUM', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Motorcycle Registration CNR (Owned or Gifted by RE)', itemCode: '10', taxationType: 'GST', sapRefNo: 'ZMRC', creditPostingOn: 'Vehicle' },
|
{ title: 'Motorcycle Registration CNR (Owned or Gifted by RE)', itemCode: '10', taxationType: 'GST', sapRefNo: 'ZMRC', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Legal Claims Reimbursement', itemCode: '11', taxationType: 'Non GST', sapRefNo: 'ZLCR', creditPostingOn: 'Vehicle' },
|
{ title: 'Legal Claims Reimbursement', itemCode: '11', taxationType: 'Non GST', sapRefNo: 'ZLCR', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Service Camp Claims', itemCode: '12', taxationType: 'Non GST', sapRefNo: 'ZSCC', creditPostingOn: 'Spares' },
|
{ title: 'Service Camp Claims', itemCode: '12', taxationType: 'Non GST', sapRefNo: 'ZSCC', creditPostingOn: 'Spares' },
|
||||||
{ title: 'Corporate Claims – Institutional Sales PD', itemCode: '13', taxationType: 'Non GST', sapRefNo: 'ZCCN', creditPostingOn: 'Vehicle' },
|
{ title: 'Corporate Claims – Institutional Sales PD', itemCode: '13', taxationType: 'Non GST', sapRefNo: 'ZCCN', creditPostingOn: 'Vehicles' },
|
||||||
{ title: 'Corporate Claims – Institutional Sales PD', itemCode: '14', taxationType: 'GST', sapRefNo: 'ZCCG', creditPostingOn: 'Vehicle' }
|
{ title: 'Corporate Claims – Institutional Sales PD', itemCode: '14', taxationType: 'GST', sapRefNo: 'ZCCG', creditPostingOn: 'Vehicles' }
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,6 +6,27 @@ import { LoginResponse } from '../types/auth.types';
|
|||||||
import logger, { logAuthEvent } from '../utils/logger';
|
import logger, { logAuthEvent } from '../utils/logger';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { emitToUser } from '../realtime/socket';
|
||||||
|
|
||||||
|
function parseDeviceFromUserAgent(ua?: string): string {
|
||||||
|
if (!ua) return 'Unknown Device';
|
||||||
|
let browser = 'Unknown Browser';
|
||||||
|
if (ua.includes('Firefox/')) browser = 'Firefox';
|
||||||
|
else if (ua.includes('Chrome/') && !ua.includes('Edg/')) browser = 'Chrome';
|
||||||
|
else if (ua.includes('Safari/') && !ua.includes('Chrome/')) browser = 'Safari';
|
||||||
|
else if (ua.includes('Edg/')) browser = 'Edge';
|
||||||
|
|
||||||
|
let os = 'Unknown OS';
|
||||||
|
if (ua.includes('Windows')) os = 'Windows';
|
||||||
|
else if (ua.includes('Mac OS')) os = 'macOS';
|
||||||
|
else if (ua.includes('Linux')) os = 'Linux';
|
||||||
|
else if (ua.includes('Android')) os = 'Android';
|
||||||
|
else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
|
||||||
|
|
||||||
|
return `${browser} on ${os}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
/**
|
/**
|
||||||
@ -240,7 +261,7 @@ export class AuthService {
|
|||||||
* Handle SSO callback from frontend
|
* Handle SSO callback from frontend
|
||||||
* Creates new user or updates existing user based on employeeId
|
* Creates new user or updates existing user based on employeeId
|
||||||
*/
|
*/
|
||||||
async handleSSOCallback(userData: SSOUserData): Promise<LoginResponse> {
|
async handleSSOCallback(userData: SSOUserData, userAgent?: string): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
// Validate required fields - email and oktaSub are required
|
// Validate required fields - email and oktaSub are required
|
||||||
if (!userData.email || !userData.oktaSub) {
|
if (!userData.email || !userData.oktaSub) {
|
||||||
@ -272,11 +293,16 @@ export class AuthService {
|
|||||||
displayName = userData.email.split('@')[0] || 'User';
|
displayName = userData.email.split('@')[0] || 'User';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionToken = uuidv4();
|
||||||
|
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
||||||
|
|
||||||
// Prepare update/create data - always include required fields
|
// Prepare update/create data - always include required fields
|
||||||
const userUpdateData: any = {
|
const userUpdateData: any = {
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
oktaSub: userData.oktaSub,
|
oktaSub: userData.oktaSub,
|
||||||
lastLogin: new Date(),
|
lastLogin: new Date(),
|
||||||
|
sessionToken,
|
||||||
|
lastLoginDevice,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -315,6 +341,14 @@ export class AuthService {
|
|||||||
action: 'user_updated',
|
action: 'user_updated',
|
||||||
updatedFields: Object.keys(userUpdateData),
|
updatedFields: Object.keys(userUpdateData),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify previous session via WebSocket to logout immediately
|
||||||
|
// This provides real-time "Last-In-Wins" enforcement
|
||||||
|
emitToUser(user.userId, 'SESSION_SUPERSEDED', {
|
||||||
|
reason: 'CONCURRENT_LOGIN',
|
||||||
|
device: lastLoginDevice,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Create new user with required fields (email and oktaSub)
|
// Create new user with required fields (email and oktaSub)
|
||||||
user = await User.create({
|
user = await User.create({
|
||||||
@ -335,7 +369,9 @@ export class AuthService {
|
|||||||
employeeNumber: userData.employeeNumber || userData.dealerCode || null,
|
employeeNumber: userData.employeeNumber || userData.dealerCode || null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
lastLogin: new Date()
|
lastLogin: new Date(),
|
||||||
|
sessionToken,
|
||||||
|
lastLoginDevice
|
||||||
});
|
});
|
||||||
|
|
||||||
logAuthEvent('sso_callback', user.userId, {
|
logAuthEvent('sso_callback', user.userId, {
|
||||||
@ -390,7 +426,8 @@ export class AuthService {
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
employeeId: user.employeeId,
|
employeeId: user.employeeId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role // Keep uppercase: USER, MANAGEMENT, ADMIN
|
role: user.role, // Keep uppercase: USER, MANAGEMENT, ADMIN
|
||||||
|
sessionToken: user.sessionToken
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: SignOptions = {
|
const options: SignOptions = {
|
||||||
@ -410,7 +447,8 @@ export class AuthService {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
type: 'refresh'
|
type: 'refresh',
|
||||||
|
sessionToken: user.sessionToken
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: SignOptions = {
|
const options: SignOptions = {
|
||||||
@ -447,6 +485,10 @@ export class AuthService {
|
|||||||
throw new Error('User not found or inactive');
|
throw new Error('User not found or inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decoded.sessionToken !== user.sessionToken) {
|
||||||
|
throw new Error('Session expired due to login from another device');
|
||||||
|
}
|
||||||
|
|
||||||
return this.generateAccessToken(user);
|
return this.generateAccessToken(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAuthEvent('auth_failure', undefined, {
|
logAuthEvent('auth_failure', undefined, {
|
||||||
@ -497,7 +539,7 @@ export class AuthService {
|
|||||||
* 4. Create/update user in our database if needed
|
* 4. Create/update user in our database if needed
|
||||||
* 5. Return our JWT tokens
|
* 5. Return our JWT tokens
|
||||||
*/
|
*/
|
||||||
async authenticateWithPassword(username: string, password: string): Promise<LoginResponse> {
|
async authenticateWithPassword(username: string, password: string, userAgent?: string): Promise<LoginResponse> {
|
||||||
// Demo admin: admin@example.com / Admin@123 (works with or without .env; for dev/demo only)
|
// Demo admin: admin@example.com / Admin@123 (works with or without .env; for dev/demo only)
|
||||||
const DEMO_ADMIN_EMAIL = 'admin@example.com';
|
const DEMO_ADMIN_EMAIL = 'admin@example.com';
|
||||||
const DEFAULT_DEMO_ADMIN_HASH = '$2a$10$H4ikTC.HDZPM0iFxjBy2C./WlkbGbidipIiZlXIJx6QpcBazdf12K'; // bcrypt of "Admin@123"
|
const DEFAULT_DEMO_ADMIN_HASH = '$2a$10$H4ikTC.HDZPM0iFxjBy2C./WlkbGbidipIiZlXIJx6QpcBazdf12K'; // bcrypt of "Admin@123"
|
||||||
@ -509,6 +551,9 @@ export class AuthService {
|
|||||||
const passwordMatch = await bcrypt.compare(password, hash);
|
const passwordMatch = await bcrypt.compare(password, hash);
|
||||||
if (!passwordMatch) return null;
|
if (!passwordMatch) return null;
|
||||||
let user = await User.findOne({ where: { email: adminEmail } });
|
let user = await User.findOne({ where: { email: adminEmail } });
|
||||||
|
const sessionToken = uuidv4();
|
||||||
|
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await User.create({
|
user = await User.create({
|
||||||
email: adminEmail,
|
email: adminEmail,
|
||||||
@ -521,10 +566,13 @@ export class AuthService {
|
|||||||
emailNotificationsEnabled: true,
|
emailNotificationsEnabled: true,
|
||||||
pushNotificationsEnabled: true,
|
pushNotificationsEnabled: true,
|
||||||
inAppNotificationsEnabled: true,
|
inAppNotificationsEnabled: true,
|
||||||
|
sessionToken,
|
||||||
|
lastLoginDevice,
|
||||||
|
lastLogin: new Date()
|
||||||
});
|
});
|
||||||
logger.info('Demo admin user created on first login', { email: adminEmail });
|
logger.info('Demo admin user created on first login', { email: adminEmail });
|
||||||
} else {
|
} else {
|
||||||
await user.update({ lastLogin: new Date() });
|
await user.update({ lastLogin: new Date(), sessionToken, lastLoginDevice });
|
||||||
}
|
}
|
||||||
logger.info('Demo admin login successful', { email: adminEmail });
|
logger.info('Demo admin login successful', { email: adminEmail });
|
||||||
const accessToken = this.generateAccessToken(user);
|
const accessToken = this.generateAccessToken(user);
|
||||||
@ -563,7 +611,7 @@ export class AuthService {
|
|||||||
displayName: 'Test Reflow Dealer',
|
displayName: 'Test Reflow Dealer',
|
||||||
firstName: 'Test',
|
firstName: 'Test',
|
||||||
lastName: 'Reflow',
|
lastName: 'Reflow',
|
||||||
});
|
}, userAgent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback bcrypt hash for "Test@123" when .env hash is corrupted (dev only)
|
// Fallback bcrypt hash for "Test@123" when .env hash is corrupted (dev only)
|
||||||
@ -608,7 +656,9 @@ export class AuthService {
|
|||||||
logger.warn('Local dealer login by email: user not found', { email });
|
logger.warn('Local dealer login by email: user not found', { email });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await user.update({ lastLogin: new Date() });
|
const sessionToken = uuidv4();
|
||||||
|
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
||||||
|
await user.update({ lastLogin: new Date(), sessionToken, lastLoginDevice });
|
||||||
logger.info('Local dealer login by email successful', { email });
|
logger.info('Local dealer login by email successful', { email });
|
||||||
const accessToken = this.generateAccessToken(user);
|
const accessToken = this.generateAccessToken(user);
|
||||||
const refreshToken = this.generateRefreshToken(user);
|
const refreshToken = this.generateRefreshToken(user);
|
||||||
@ -735,7 +785,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Step 4: Create/update user in our database
|
// Step 4: Create/update user in our database
|
||||||
const result = await this.handleSSOCallback(userData);
|
const result = await this.handleSSOCallback(userData, userAgent);
|
||||||
|
|
||||||
logger.info('User authenticated successfully via password flow', {
|
logger.info('User authenticated successfully via password flow', {
|
||||||
userId: result.user.userId,
|
userId: result.user.userId,
|
||||||
@ -791,7 +841,7 @@ export class AuthService {
|
|||||||
* This is the FRONTEND callback URL (e.g., http://localhost:3000/login/callback),
|
* This is the FRONTEND callback URL (e.g., http://localhost:3000/login/callback),
|
||||||
* NOT the backend URL. Okta verifies this matches to prevent redirect URI attacks.
|
* NOT the backend URL. Okta verifies this matches to prevent redirect URI attacks.
|
||||||
*/
|
*/
|
||||||
async exchangeCodeForTokens(code: string, redirectUri: string): Promise<LoginResponse> {
|
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
// Validate configuration
|
// Validate configuration
|
||||||
if (!ssoConfig.oktaClientId || ssoConfig.oktaClientId.trim() === '') {
|
if (!ssoConfig.oktaClientId || ssoConfig.oktaClientId.trim() === '') {
|
||||||
@ -926,7 +976,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle SSO callback to create/update user and generate our tokens
|
// Handle SSO callback to create/update user and generate our tokens
|
||||||
const result = await this.handleSSOCallback(userData);
|
const result = await this.handleSSOCallback(userData, userAgent);
|
||||||
|
|
||||||
// Return our JWT tokens along with Okta tokens (store Okta refresh token for future use)
|
// Return our JWT tokens along with Okta tokens (store Okta refresh token for future use)
|
||||||
return {
|
return {
|
||||||
@ -970,7 +1020,7 @@ export class AuthService {
|
|||||||
* Exchange Tanflow authorization code for tokens
|
* Exchange Tanflow authorization code for tokens
|
||||||
* Similar to Okta flow but uses Tanflow IAM endpoints
|
* Similar to Okta flow but uses Tanflow IAM endpoints
|
||||||
*/
|
*/
|
||||||
async exchangeTanflowCodeForTokens(code: string, redirectUri: string): Promise<LoginResponse> {
|
async exchangeTanflowCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
// Validate configuration
|
// Validate configuration
|
||||||
if (!ssoConfig.tanflowClientId || ssoConfig.tanflowClientId.trim() === '') {
|
if (!ssoConfig.tanflowClientId || ssoConfig.tanflowClientId.trim() === '') {
|
||||||
@ -1138,7 +1188,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle SSO callback to create/update user and generate our tokens
|
// Handle SSO callback to create/update user and generate our tokens
|
||||||
const result = await this.handleSSOCallback(userData);
|
const result = await this.handleSSOCallback(userData, userAgent);
|
||||||
|
|
||||||
// Return our JWT tokens along with Tanflow tokens
|
// Return our JWT tokens along with Tanflow tokens
|
||||||
return {
|
return {
|
||||||
|
|||||||
161
src/services/creditNoteSync.service.ts
Normal file
161
src/services/creditNoteSync.service.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { wfmFileService } from './wfmFile.service';
|
||||||
|
import { WorkflowRequest } from '../models/WorkflowRequest';
|
||||||
|
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
||||||
|
import { ClaimCreditNoteItem } from '../models/ClaimCreditNoteItem';
|
||||||
|
import { sequelize } from '@config/database';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export class CreditNoteSyncService {
|
||||||
|
/**
|
||||||
|
* Main sync function to process all outgoing files
|
||||||
|
*/
|
||||||
|
async syncCreditNotes(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const gstFiles = wfmFileService.listOutgoingFiles(false);
|
||||||
|
const nonGstFiles = wfmFileService.listOutgoingFiles(true);
|
||||||
|
|
||||||
|
const allFiles = [
|
||||||
|
...gstFiles.map(f => ({ path: f, isNonGst: false })),
|
||||||
|
...nonGstFiles.map(f => ({ path: f, isNonGst: true }))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allFiles.length === 0) return;
|
||||||
|
|
||||||
|
logger.info(`[CreditNoteSyncService] Found ${allFiles.length} files to process`);
|
||||||
|
|
||||||
|
for (const fileInfo of allFiles) {
|
||||||
|
await this.processFile(fileInfo.path);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[CreditNoteSyncService] Error during sync:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single CSV file
|
||||||
|
*/
|
||||||
|
async processFile(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return false;
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const lines = fileContent.split(/\r?\n/).filter(l => l.trim() !== '');
|
||||||
|
if (lines.length <= 1) {
|
||||||
|
// Empty or only headers - delete it
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
logger.info(`[CreditNoteSyncService] Deleted empty/header-only file: ${filePath}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = lines[0].split('|').map(h => h.trim().toUpperCase());
|
||||||
|
const rows = lines.slice(1).map(line => {
|
||||||
|
const values = line.split('|');
|
||||||
|
const row: any = {};
|
||||||
|
headers.forEach((h, i) => { row[h] = values[i]?.trim() || ''; });
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group rows by CLAIM_NUMBER
|
||||||
|
const groups: Record<string, any[]> = {};
|
||||||
|
rows.forEach(row => {
|
||||||
|
const claimNum = row.CLAIM_NUMBER;
|
||||||
|
if (!claimNum) return;
|
||||||
|
if (!groups[claimNum]) groups[claimNum] = [];
|
||||||
|
groups[claimNum].push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each group
|
||||||
|
let allProcessed = true;
|
||||||
|
for (const [claimNumber, rows] of Object.entries(groups)) {
|
||||||
|
const success = await this.processClaimGroup(claimNumber, rows, filePath);
|
||||||
|
if (!success) {
|
||||||
|
allProcessed = false;
|
||||||
|
logger.warn(`[CreditNoteSyncService] Failed to process claim group ${claimNumber} in file ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allProcessed && rows.length > 0) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
logger.info(`[CreditNoteSyncService] Successfully processed and deleted file: ${filePath}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[CreditNoteSyncService] Error processing file ${filePath}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processClaimGroup(claimNumber: string, rows: any[], filePath: string): Promise<boolean> {
|
||||||
|
const t = await sequelize.transaction();
|
||||||
|
try {
|
||||||
|
// 1. Find the request by requestNumber (which is the CLAIM_NUMBER in CSV)
|
||||||
|
const request = await WorkflowRequest.findOne({ where: { requestNumber: claimNumber }, transaction: t });
|
||||||
|
if (!request) {
|
||||||
|
logger.warn(`[CreditNoteSyncService] WorkflowRequest not found for claim number: ${claimNumber}`);
|
||||||
|
await t.rollback();
|
||||||
|
// We return true here because we might still want to delete the file if other claims are processed
|
||||||
|
// or if this is a filtered/old claim we don't care about.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = request.requestId;
|
||||||
|
|
||||||
|
// 2. Calculate totals
|
||||||
|
let totalAmount = 0;
|
||||||
|
let totalTds = 0;
|
||||||
|
let totalCredit = 0;
|
||||||
|
rows.forEach(row => {
|
||||||
|
totalAmount += Number(row.CLAIM_AMT || row.CREDIT_AMT || 0);
|
||||||
|
totalTds += Number(row.TDS_AMT || 0);
|
||||||
|
totalCredit += Number(row.CREDIT_AMT || row.FINAL_AMT || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRow = rows[0];
|
||||||
|
|
||||||
|
// 3. Upsert Header
|
||||||
|
const [cnHeader] = await ClaimCreditNote.upsert({
|
||||||
|
requestId,
|
||||||
|
creditNoteNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || undefined,
|
||||||
|
sapDocumentNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || undefined,
|
||||||
|
status: firstRow.MSG_TYP || 'CONFIRMED',
|
||||||
|
errorMessage: firstRow.MESSAGE || undefined,
|
||||||
|
creditNoteFilePath: filePath,
|
||||||
|
creditNoteAmount: totalAmount,
|
||||||
|
transactionNo: firstRow.TRNS_UNIQ_NO || undefined,
|
||||||
|
tdsAmount: totalTds,
|
||||||
|
creditAmount: totalCredit,
|
||||||
|
confirmedAt: new Date()
|
||||||
|
}, { transaction: t, returning: true });
|
||||||
|
|
||||||
|
// 4. Update Line Items
|
||||||
|
// Clear existing items
|
||||||
|
await ClaimCreditNoteItem.destroy({ where: { creditNoteId: cnHeader.creditNoteId }, transaction: t });
|
||||||
|
|
||||||
|
// Bulk create new items
|
||||||
|
const itemsToCreate = rows.map((row, index) => ({
|
||||||
|
creditNoteId: cnHeader.creditNoteId,
|
||||||
|
slNo: index + 1,
|
||||||
|
transactionNo: row.TRNS_UNIQ_NO,
|
||||||
|
description: row.DESCRIPTION || row.MESSAGE || '',
|
||||||
|
hsnCd: row.HSN_CODE || row.SAC_CODE || '',
|
||||||
|
amount: Number(row.FINAL_AMT || row.CREDIT_AMT || 0),
|
||||||
|
claimAmount: Number(row.CLAIM_AMT || 0),
|
||||||
|
tdsAmount: Number(row.TDS_AMT || 0),
|
||||||
|
creditAmount: Number(row.FINAL_AMT || row.CREDIT_AMT || 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await ClaimCreditNoteItem.bulkCreate(itemsToCreate, { transaction: t });
|
||||||
|
|
||||||
|
await t.commit();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (t) await t.rollback();
|
||||||
|
logger.error(`[CreditNoteSyncService] Error processing claim ${claimNumber}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const creditNoteSyncService = new CreditNoteSyncService();
|
||||||
@ -10,6 +10,7 @@ import { InternalOrder, IOStatus } from '../models/InternalOrder';
|
|||||||
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
|
import { ClaimBudgetTracking, BudgetStatus } from '../models/ClaimBudgetTracking';
|
||||||
import { ClaimInvoice } from '../models/ClaimInvoice';
|
import { ClaimInvoice } from '../models/ClaimInvoice';
|
||||||
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
||||||
|
import { ClaimCreditNoteItem } from '../models/ClaimCreditNoteItem';
|
||||||
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
||||||
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
|
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
|
||||||
import { ApprovalLevel } from '../models/ApprovalLevel';
|
import { ApprovalLevel } from '../models/ApprovalLevel';
|
||||||
@ -1249,9 +1250,10 @@ export class DealerClaimService {
|
|||||||
where: { requestId }
|
where: { requestId }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Credit Note details
|
// Fetch Credit Note details with items
|
||||||
const claimCreditNote = await ClaimCreditNote.findOne({
|
const claimCreditNote = await ClaimCreditNote.findOne({
|
||||||
where: { requestId }
|
where: { requestId },
|
||||||
|
include: [{ model: ClaimCreditNoteItem, as: 'items' }]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Completion Expenses (individual expense items)
|
// Fetch Completion Expenses (individual expense items)
|
||||||
|
|||||||
158
src/services/hsnSacCode.service.ts
Normal file
158
src/services/hsnSacCode.service.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { HsnSacCode, HsnSacCodeAttributes, HsnSacCodeCreationAttributes } from '../models/HsnSacCode';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
export interface PaginatedHsnSacCodes {
|
||||||
|
codes: HsnSacCode[];
|
||||||
|
pagination: {
|
||||||
|
totalRecords: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HsnSacCodeService {
|
||||||
|
/**
|
||||||
|
* Get HSN/SAC codes with pagination and search
|
||||||
|
*/
|
||||||
|
async getAllCodes(
|
||||||
|
onlyActive: boolean = false,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10,
|
||||||
|
search?: string
|
||||||
|
): Promise<PaginatedHsnSacCodes> {
|
||||||
|
try {
|
||||||
|
const where: any = {};
|
||||||
|
if (onlyActive) {
|
||||||
|
where.isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search && search.trim()) {
|
||||||
|
const searchTerm = `%${search.trim()}%`;
|
||||||
|
where[Op.or] = [
|
||||||
|
{ code: { [Op.like]: searchTerm } },
|
||||||
|
{ description: { [Op.like]: searchTerm } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const { count, rows } = await HsnSacCode.findAndCountAll({
|
||||||
|
where,
|
||||||
|
order: [['type', 'ASC'], ['code', 'ASC']],
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
codes: rows,
|
||||||
|
pagination: {
|
||||||
|
totalRecords: count,
|
||||||
|
totalPages: Math.ceil(count / limit),
|
||||||
|
currentPage: page,
|
||||||
|
limit
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching HSN/SAC codes:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single code by ID
|
||||||
|
*/
|
||||||
|
async getCodeById(id: string): Promise<HsnSacCode | null> {
|
||||||
|
try {
|
||||||
|
return await HsnSacCode.findByPk(id);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching HSN/SAC code with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new HSN/SAC code with duplicate check
|
||||||
|
*/
|
||||||
|
async createCode(data: HsnSacCodeCreationAttributes): Promise<HsnSacCode> {
|
||||||
|
try {
|
||||||
|
// Check for duplicates
|
||||||
|
const existing = await HsnSacCode.findOne({
|
||||||
|
where: {
|
||||||
|
code: data.code,
|
||||||
|
type: data.type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new Error(`${data.type} code "${data.code}" already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await HsnSacCode.create(data);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error creating HSN/SAC code:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing HSN/SAC code with duplicate check
|
||||||
|
*/
|
||||||
|
async updateCode(id: string, data: Partial<HsnSacCodeAttributes>): Promise<HsnSacCode | null> {
|
||||||
|
try {
|
||||||
|
const code = await HsnSacCode.findByPk(id);
|
||||||
|
if (!code) return null;
|
||||||
|
|
||||||
|
// If code or type is being updated, check for duplicates
|
||||||
|
if (data.code || data.type) {
|
||||||
|
const existing = await HsnSacCode.findOne({
|
||||||
|
where: {
|
||||||
|
code: data.code || code.code,
|
||||||
|
type: data.type || code.type,
|
||||||
|
id: { [Op.ne]: id }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new Error(`${data.type || code.type} code "${data.code || code.code}" already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await code.update(data);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error updating HSN/SAC code with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an HSN/SAC code
|
||||||
|
*/
|
||||||
|
async deleteCode(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await HsnSacCode.destroy({ where: { id } });
|
||||||
|
return result > 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error deleting HSN/SAC code with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle active status
|
||||||
|
*/
|
||||||
|
async toggleActive(id: string): Promise<HsnSacCode | null> {
|
||||||
|
try {
|
||||||
|
const code = await HsnSacCode.findByPk(id);
|
||||||
|
if (!code) return null;
|
||||||
|
|
||||||
|
return await code.update({ isActive: !code.isActive });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error toggling active status for HSN/SAC code with id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsnSacCodeService = new HsnSacCodeService();
|
||||||
@ -392,7 +392,7 @@ export class PWCIntegrationService {
|
|||||||
|
|
||||||
const payload = [
|
const payload = [
|
||||||
{
|
{
|
||||||
User_GSTIN: dealerGst, // Portal User ID (Dealer's own GST)
|
User_GSTIN: '27AAAPI3182M002', // Portal User ID (Dealer's own GST)
|
||||||
Version: "1.01",
|
Version: "1.01",
|
||||||
IRN: "",
|
IRN: "",
|
||||||
SourceSystem: "RE_WORKFLOW",
|
SourceSystem: "RE_WORKFLOW",
|
||||||
@ -422,13 +422,13 @@ export class PWCIntegrationService {
|
|||||||
Dt: new Date().toLocaleDateString('en-GB') // DD/MM/YYYY
|
Dt: new Date().toLocaleDateString('en-GB') // DD/MM/YYYY
|
||||||
},
|
},
|
||||||
SellerDtls: {
|
SellerDtls: {
|
||||||
Gstin: dealerGst, // Actual dealer GST from local table
|
Gstin: '27AAAPI3182M002', // Actual dealer GST from local table
|
||||||
LglNm: dealer?.dealerName || 'Dealer',
|
LglNm: dealer?.dealerName || 'Dealer',
|
||||||
TrdNm: dealer?.dealerName || 'Dealer',
|
TrdNm: dealer?.dealerName || 'Dealer',
|
||||||
Addr1: dealer?.city || "Address Line 1",
|
Addr1: dealer?.city || "Address Line 1",
|
||||||
Loc: dealer?.city || "Location",
|
Loc: dealer?.city || "Location",
|
||||||
Pin: Number(dealer?.pincode || 600001),
|
Pin: 400001,
|
||||||
Stcd: dealerStateCode,
|
Stcd: '27',
|
||||||
Ph: dealer?.phone || "9998887776",
|
Ph: dealer?.phone || "9998887776",
|
||||||
Em: dealer?.email || "Supplier@inv.com"
|
Em: dealer?.email || "Supplier@inv.com"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -159,6 +159,18 @@ export class WFMFileService {
|
|||||||
return path.join(this.basePath, targetPath);
|
return path.join(this.basePath, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all outgoing CSV files in the folder.
|
||||||
|
*/
|
||||||
|
listOutgoingFiles(isNonGst: boolean = false): string[] {
|
||||||
|
const outgoingDir = this.getOutgoingClaimsDir(isNonGst);
|
||||||
|
if (!fs.existsSync(outgoingDir)) return [];
|
||||||
|
|
||||||
|
return fs.readdirSync(outgoingDir)
|
||||||
|
.filter(file => file.toLowerCase().endsWith('.csv'))
|
||||||
|
.map(file => path.join(outgoingDir, file));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build outgoing credit note file path for a dealer + request.
|
* Build outgoing credit note file path for a dealer + request.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -39,6 +39,12 @@ export interface ApiResponse<T = any> {
|
|||||||
message: string;
|
message: string;
|
||||||
data?: T;
|
data?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
pagination?: {
|
||||||
|
totalRecords: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,16 +7,22 @@ export class ResponseHandler {
|
|||||||
res: Response,
|
res: Response,
|
||||||
data: T,
|
data: T,
|
||||||
message: string = 'Success',
|
message: string = 'Success',
|
||||||
statusCode: number = 200
|
statusCode: number = 200,
|
||||||
|
pagination?: {
|
||||||
|
totalRecords: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
): void {
|
): void {
|
||||||
const response: ApiResponse<T> = {
|
const response: ApiResponse<T> = {
|
||||||
success: true,
|
success: true,
|
||||||
message,
|
message,
|
||||||
data,
|
data,
|
||||||
|
pagination,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
res.status(statusCode).json(response);
|
res.status(statusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,8 +58,8 @@ export const createActivityTypeSchema = z.object({
|
|||||||
errorMap: () => ({ message: 'Taxation type must be GST or Non GST' }),
|
errorMap: () => ({ message: 'Taxation type must be GST or Non GST' }),
|
||||||
}),
|
}),
|
||||||
sapRefNo: z.string().min(1, 'SAP ref number (Claim Document Type) is required').max(50, 'SAP ref number too long'),
|
sapRefNo: z.string().min(1, 'SAP ref number (Claim Document Type) is required').max(50, 'SAP ref number too long'),
|
||||||
creditPostingOn: z.enum(['Spares', 'Vehicle', 'GMA', 'Apparel'], {
|
creditPostingOn: z.enum(['Spares', 'Vehicle', 'Vehicles', 'GMA', 'Apparel'], {
|
||||||
errorMap: () => ({ message: 'Credit posting on must be Spares, Vehicle, GMA or Apparel' }),
|
errorMap: () => ({ message: 'Credit posting on must be Spares, Vehicle, Vehicles, GMA or Apparel' }),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user