SAP team asked to map credit posting on based on the item group Vehicle/Spares have been implemented also points like delare code pading and nomenclature changes added

This commit is contained in:
laxmanhalaki 2026-03-17 17:20:25 +05:30
parent 64e8c2237a
commit 0f99fe68d5
21 changed files with 270 additions and 124 deletions

View File

@ -1 +1 @@
import{a as s}from"./index-DsQZmIYq.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
import{a as s}from"./index-BgKXDGEk.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@
<!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-DsQZmIYq.js"></script>
<script type="module" crossorigin src="/assets/index-BgKXDGEk.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
@ -21,7 +21,7 @@
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-BATWUvr6.js">
<link rel="stylesheet" crossorigin href="/assets/index-Bgoo5ePN.css">
<link rel="stylesheet" crossorigin href="/assets/index-D2NzWWdB.css">
</head>
<body>

View File

@ -1189,6 +1189,7 @@ export const createActivityType = async (req: Request, res: Response): Promise<v
itemCode: itemCode || null,
taxationType: taxationType || null,
sapRefNo: sapRefNo || null,
creditPostingOn: req.body.creditPostingOn || null,
createdBy: userId
});

View File

@ -18,6 +18,7 @@ import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
import { ActivityType } from '../models/ActivityType';
import { Participant } from '../models/Participant';
import { sanitizeObject, sanitizePermissive } from '../utils/sanitizer';
import { padDealerCode } from '../utils/helpers';
export class DealerClaimController {
private dealerClaimService = new DealerClaimService();
@ -1109,7 +1110,7 @@ export class DealerClaimController {
const trnsUniqNo = item.transactionCode || '';
const claimNumber = requestNumber;
const invNumber = invoice?.invoiceNumber || '';
const dealerCode = claimDetails?.dealerCode || '';
const dealerCode = padDealerCode(claimDetails?.dealerCode || '');
const ioNumber = internalOrder?.ioNumber || '';
const claimDocTyp = sapRefNo;
const claimType = claimDetails?.activityType || '';

View File

@ -0,0 +1,86 @@
import { QueryInterface, DataTypes } from 'sequelize';
/**
* Helper: returns true if the column exists in the table
*/
async function columnExists(
queryInterface: QueryInterface,
tableName: string,
columnName: string
): Promise<boolean> {
try {
const description = await queryInterface.describeTable(tableName);
return columnName in description;
} catch {
return false;
}
}
/**
* Migration: Refactor activity_types table
*
* Drops deprecated columns that will not be used going forward:
* hsn_code, sac_code, gst_rate, gl_code, credit_nature
*
* Adds new column:
* credit_posting_on VARCHAR(50) indicates posting target (e.g. 'Spares', 'Vehicle')
*
* All drops are guarded so this migration is safe to run on a fresh database
* where these columns were never added.
*/
export async function up(queryInterface: QueryInterface): Promise<void> {
const TABLE = 'activity_types';
// ── Drop deprecated columns (safe: only if they exist) ──────────────────────
const columnsToDrop = ['hsn_code', 'sac_code', 'gst_rate', 'gl_code', 'credit_nature'];
for (const col of columnsToDrop) {
if (await columnExists(queryInterface, TABLE, col)) {
await queryInterface.removeColumn(TABLE, col);
console.log(`[Migration] Dropped column ${TABLE}.${col}`);
} else {
console.log(`[Migration] Column ${TABLE}.${col} does not exist skipping drop`);
}
}
// ── Add new column ────────────────────────────────────────────────────────────
if (!(await columnExists(queryInterface, TABLE, 'credit_posting_on'))) {
await queryInterface.addColumn(TABLE, 'credit_posting_on', {
type: DataTypes.STRING(50),
allowNull: true,
defaultValue: null,
comment: 'Indicates what the credit note is posted against (e.g. "Spares", "Vehicle")'
});
console.log(`[Migration] Added column ${TABLE}.credit_posting_on`);
} else {
console.log(`[Migration] Column ${TABLE}.credit_posting_on already exists skipping add`);
}
}
/**
* Rollback: re-add the dropped columns and remove credit_posting_on.
* Columns are restored as nullable so existing rows are unaffected.
*/
export async function down(queryInterface: QueryInterface): Promise<void> {
const TABLE = 'activity_types';
// Remove the newly added column
if (await columnExists(queryInterface, TABLE, 'credit_posting_on')) {
await queryInterface.removeColumn(TABLE, 'credit_posting_on');
}
// Restore dropped columns
const columnsToRestore: Record<string, any> = {
hsn_code: { type: DataTypes.STRING(20), allowNull: true, defaultValue: null },
sac_code: { type: DataTypes.STRING(20), allowNull: true, defaultValue: null },
gst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true, defaultValue: null },
gl_code: { type: DataTypes.STRING(20), allowNull: true, defaultValue: null },
credit_nature: { type: DataTypes.STRING(50), allowNull: true, defaultValue: null }
};
for (const [col, spec] of Object.entries(columnsToRestore)) {
if (!(await columnExists(queryInterface, TABLE, col))) {
await queryInterface.addColumn(TABLE, col, spec);
}
}
}

View File

@ -9,18 +9,14 @@ interface ActivityTypeAttributes {
taxationType?: string;
sapRefNo?: string;
isActive: boolean;
hsnCode?: string | null;
sacCode?: string | null;
gstRate?: number | null;
glCode?: string | null;
creditNature?: 'Commercial' | 'GST' | null;
creditPostingOn?: string | null;
createdBy: string;
updatedBy?: string;
createdAt: Date;
updatedAt: Date;
}
interface ActivityTypeCreationAttributes extends Optional<ActivityTypeAttributes, 'activityTypeId' | 'itemCode' | 'taxationType' | 'sapRefNo' | 'isActive' | 'updatedBy' | 'createdAt' | 'updatedAt'> { }
interface ActivityTypeCreationAttributes extends Optional<ActivityTypeAttributes, 'activityTypeId' | 'itemCode' | 'taxationType' | 'sapRefNo' | 'isActive' | 'creditPostingOn' | 'updatedBy' | 'createdAt' | 'updatedAt'> { }
class ActivityType extends Model<ActivityTypeAttributes, ActivityTypeCreationAttributes> implements ActivityTypeAttributes {
public activityTypeId!: string;
@ -29,11 +25,7 @@ class ActivityType extends Model<ActivityTypeAttributes, ActivityTypeCreationAtt
public taxationType?: string;
public sapRefNo?: string;
public isActive!: boolean;
public hsnCode?: string | null;
public sacCode?: string | null;
public gstRate?: number | null;
public glCode?: string | null;
public creditNature?: 'Commercial' | 'GST' | null;
public creditPostingOn?: string | null;
public createdBy!: string;
public updatedBy?: string;
public createdAt!: Date;
@ -81,30 +73,12 @@ ActivityType.init(
defaultValue: true,
field: 'is_active'
},
hsnCode: {
type: DataTypes.STRING(20),
allowNull: true,
field: 'hsn_code'
},
sacCode: {
type: DataTypes.STRING(20),
allowNull: true,
field: 'sac_code'
},
gstRate: {
type: DataTypes.DECIMAL(5, 2),
allowNull: true,
field: 'gst_rate'
},
glCode: {
creditPostingOn: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'gl_code'
},
creditNature: {
type: DataTypes.ENUM('Commercial', 'GST'),
allowNull: true,
field: 'credit_nature'
defaultValue: null,
field: 'credit_posting_on',
comment: 'Indicates what the credit note is posted against (e.g. "Spares", "Vehicle")'
},
createdBy: {
type: DataTypes.UUID,

View File

@ -42,7 +42,8 @@ router.get('/activity-types',
title: at.title,
itemCode: at.itemCode,
taxationType: at.taxationType,
sapRefNo: at.sapRefNo
sapRefNo: at.sapRefNo,
creditPostingOn: at.creditPostingOn
}))
});
return;

View File

@ -175,6 +175,7 @@ async function runMigrations(): Promise<void> {
const m58 = require('../migrations/20260303100001-drop-form16a-number-unique');
const m59 = require('../migrations/20260309-add-wfm-push-fields');
const m60 = require('../migrations/20260316-update-holiday-type-enum');
const m61 = require('../migrations/20260317-refactor-activity-types-columns');
const migrations = [
{ name: '2025103000-create-users', module: m0 },
@ -242,6 +243,7 @@ async function runMigrations(): Promise<void> {
{ name: '20260303100001-drop-form16a-number-unique', module: m58 },
{ name: '20260309-add-wfm-push-fields', module: m59 },
{ name: '20260316-update-holiday-type-enum', module: m60 },
{ name: '20260317-refactor-activity-types-columns', module: m61 },
];
// Dynamically import sequelize after secrets are loaded

View File

@ -65,6 +65,7 @@ import * as m57 from '../migrations/20260225100001-add-form16-archived-at';
import * as m58 from '../migrations/20260303100001-drop-form16a-number-unique';
import * as m59 from '../migrations/20260309-add-wfm-push-fields';
import * as m60 from '../migrations/20260316-update-holiday-type-enum';
import * as m61 from '../migrations/20260317-refactor-activity-types-columns';
interface Migration {
name: string;
@ -137,6 +138,7 @@ const migrations: Migration[] = [
{ name: '20260303100001-drop-form16a-number-unique', module: m58 },
{ name: '20260309-add-wfm-push-fields', module: m59 },
{ name: '20260316-update-holiday-type-enum', module: m60 },
{ name: '20260317-refactor-activity-types-columns', module: m61 },
];

View File

@ -60,14 +60,12 @@ export class ActivityTypeService {
}
}
/**
* Create a new activity type
*/
async createActivityType(activityTypeData: {
title: string;
itemCode?: string;
taxationType?: string;
sapRefNo?: string;
creditPostingOn?: string;
createdBy: string;
}): Promise<ActivityType> {
try {
@ -104,6 +102,7 @@ export class ActivityTypeService {
itemCode?: string;
taxationType?: string;
sapRefNo?: string;
creditPostingOn?: string;
isActive?: boolean;
}, updatedBy: string): Promise<ActivityType | null> {
try {

View File

@ -8,19 +8,20 @@ import { ActivityType } from '@models/ActivityType';
* These will be seeded into the database with default item_code values (1-13)
*/
const DEFAULT_ACTIVITY_TYPES = [
{ title: 'Riders Mania Claims', itemCode: '1', taxationType: 'Non GST', sapRefNo: 'ZRDM' },
{ title: 'Marketing Cost Bike to Vendor', itemCode: '2', taxationType: 'Non GST', sapRefNo: 'ZMBV' },
{ title: 'Media Bike Service', itemCode: '3', taxationType: 'GST', sapRefNo: 'ZMBS' },
{ title: 'ARAI Motorcycle Liquidation', itemCode: '4', taxationType: 'GST', sapRefNo: 'ZAML' },
{ title: 'ARAI Certification STA Approval CNR', itemCode: '5', taxationType: 'Non GST', sapRefNo: 'ZACS' },
{ title: 'Procurement of Spares/Apparel/GMA for Events', itemCode: '6', taxationType: 'GST', sapRefNo: 'ZPPE' },
{ title: 'Fuel for Media Bike Used for Event', itemCode: '7', taxationType: 'Non GST', sapRefNo: 'ZFMB' },
{ title: 'Motorcycle Buyback and Goodwill Support', itemCode: '8', taxationType: 'Non GST', sapRefNo: 'ZMBG' },
{ title: 'Liquidation of Used Motorcycle', itemCode: '9', taxationType: 'GST', sapRefNo: 'ZLUM' },
{ title: 'Motorcycle Registration CNR (Owned or Gifted by RE)', itemCode: '10', taxationType: 'GST', sapRefNo: 'ZMRC' },
{ title: 'Legal Claims Reimbursement', itemCode: '11', taxationType: 'Non GST', sapRefNo: 'ZLCR' },
{ title: 'Service Camp Claims', itemCode: '12', taxationType: 'Non GST', sapRefNo: 'ZSCC' },
{ title: 'Corporate Claims Institutional Sales PDI', itemCode: '13', taxationType: 'Non GST', sapRefNo: 'ZCCN' }
{ 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: '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 Certification STA Approval CNR', itemCode: '5', taxationType: 'Non GST', sapRefNo: 'ZACS', creditPostingOn: 'Vehicle' },
{ 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: 'Motorcycle Buyback and Goodwill Support', itemCode: '8', taxationType: 'Non GST', sapRefNo: 'ZMBG', creditPostingOn: 'Vehicle' },
{ title: 'Liquidation of Used Motorcycle', itemCode: '9', taxationType: 'GST', sapRefNo: 'ZLUM', creditPostingOn: 'Vehicle' },
{ title: 'Motorcycle Registration CNR (Owned or Gifted by RE)', itemCode: '10', taxationType: 'GST', sapRefNo: 'ZMRC', creditPostingOn: 'Vehicle' },
{ title: 'Legal Claims Reimbursement', itemCode: '11', taxationType: 'Non GST', sapRefNo: 'ZLCR', creditPostingOn: 'Vehicle' },
{ 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: '14', taxationType: 'GST', sapRefNo: 'ZCCG', creditPostingOn: 'Vehicle' }
];
/**
@ -70,19 +71,20 @@ export async function seedDefaultActivityTypes(): Promise<void> {
let skippedCount = 0;
for (const activityType of DEFAULT_ACTIVITY_TYPES) {
const { title, itemCode, taxationType, sapRefNo } = activityType;
const { title, itemCode, taxationType, sapRefNo, creditPostingOn } = activityType;
try {
// Check if activity type already exists (active or inactive)
const existing = await ActivityType.findOne({
where: { title }
where: { title, sapRefNo } // Match on both title and sapRefNo since title "Corporate Claims..." is duplicated
});
if (existing) {
// Identify fields to update (only need systemUserId for updatedBy)
const updates: any = {};
if (!existing.itemCode && itemCode) updates.itemCode = itemCode;
if (!existing.taxationType && taxationType) updates.taxationType = taxationType;
if (!existing.sapRefNo && sapRefNo) updates.sapRefNo = sapRefNo;
if (existing.itemCode !== itemCode) updates.itemCode = itemCode;
if (existing.taxationType !== taxationType) updates.taxationType = taxationType;
if (existing.sapRefNo !== sapRefNo) updates.sapRefNo = sapRefNo;
if (existing.creditPostingOn !== creditPostingOn) updates.creditPostingOn = creditPostingOn;
if (systemUserId) updates.updatedBy = systemUserId;
if (!existing.isActive) {
@ -94,6 +96,7 @@ export async function seedDefaultActivityTypes(): Promise<void> {
if (Object.keys(updates).length > 0) {
await existing.update(updates);
logger.debug(`[ActivityType Seed] Updated fields for existing activity type: ${title} (updates: ${JSON.stringify(updates)})`);
updatedCount++;
} else {
skippedCount++;
logger.debug(`[ActivityType Seed] Activity type already exists and active: ${title}`);
@ -111,6 +114,7 @@ export async function seedDefaultActivityTypes(): Promise<void> {
itemCode: itemCode,
taxationType: taxationType,
sapRefNo: sapRefNo,
creditPostingOn: creditPostingOn,
isActive: true,
createdBy: systemUserId!
} as any);

View File

@ -32,6 +32,7 @@ export interface DealerInfo {
dealerPrincipalEmailId?: string | null;
gstin?: string | null;
pincode?: string | null;
itemGroup?: string | null;
}
/**
@ -110,6 +111,7 @@ export async function getAllDealers(searchTerm?: string, limit: number = 10): Pr
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
pincode: dealer.showroomPincode || null,
itemGroup: null, // Local dealer table doesn't have item group yet
};
});
} catch (error) {
@ -162,6 +164,7 @@ export async function getDealerByCode(dealerCode: string): Promise<DealerInfo |
city: externalData?.['re city'] || null,
state: externalData?.['re state code'] || null,
pincode: externalData?.pincode || null,
itemGroup: externalData?.['item group'] || null,
};
}
@ -189,6 +192,7 @@ export async function getDealerByCode(dealerCode: string): Promise<DealerInfo |
city: externalData['re city'],
state: externalData['re state code'],
pincode: externalData.pincode,
itemGroup: externalData['item group'] || null,
};
}
logger.warn(`[DealerService] Dealer not found in any source: ${dealerCode}`);
@ -217,6 +221,7 @@ export async function getDealerByCode(dealerCode: string): Promise<DealerInfo |
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: externalData?.gstin || dealer.gst || null,
pincode: externalData?.pincode || dealer.showroomPincode || null,
itemGroup: externalData?.['item group'] || null,
};
} catch (error) {
logger.error('[DealerService] Error fetching dealer by code:', error);
@ -278,6 +283,7 @@ export async function getDealerByEmail(email: string): Promise<DealerInfo | null
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
pincode: dealer.showroomPincode || null,
itemGroup: null,
};
} catch (error) {
logger.error('[DealerService] Error fetching dealer by email:', error);

View File

@ -21,7 +21,7 @@ import { Document } from '../models/Document';
import { Dealer } from '../models/Dealer';
import { WorkflowService } from './workflow.service';
import { DealerClaimApprovalService } from './dealerClaimApproval.service';
import { generateRequestNumber } from '../utils/helpers';
import { generateRequestNumber, padDealerCode } from '../utils/helpers';
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
import { sapIntegrationService } from './sapIntegration.service';
import { pwcIntegrationService } from './pwcIntegration.service';
@ -125,6 +125,38 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] Validating dealer for code: ${claimData.dealerCode}`);
const dealerUser = await validateDealerUser(claimData.dealerCode);
// 0a. Validate Dealer Item Group against Activity Credit Posting
const activityType = await ActivityType.findOne({ where: { title: claimData.activityType } });
if (activityType && activityType.creditPostingOn) {
// Fetch full dealer info (including external API data like itemGroup)
const fullDealerInfo = await findDealerLocally(dealerCode);
if (fullDealerInfo && fullDealerInfo.itemGroup) {
const creditPostingOn = activityType.creditPostingOn.toLowerCase();
const itemGroup = fullDealerInfo.itemGroup.toLowerCase();
let isMatch = false;
if (creditPostingOn === 'vehicle' && itemGroup === 'vehicle') {
isMatch = true;
} else if (creditPostingOn === 'spares' && itemGroup === 'spares') {
isMatch = true;
} else if (creditPostingOn === 'gma' && itemGroup === 'gma') {
isMatch = true;
} else if (creditPostingOn === 'apparel' && itemGroup === 'apparel') {
isMatch = true;
} else if (creditPostingOn === itemGroup) {
isMatch = true; // Fallback for direct match
}
if (!isMatch) {
logger.warn(`[DealerClaimService] Validation failed: Activity ${claimData.activityType} (${creditPostingOn}) vs Dealer ${dealerCode} (${itemGroup})`);
throw new Error('incorrect Delercode for selected service group');
}
logger.info(`[DealerClaimService] Validation successful: Activity ${creditPostingOn} matched Dealer group ${itemGroup}`);
}
}
// Update claim data with validated dealer user details if not provided
claimData.dealerName = dealerUser.displayName || claimData.dealerName;
claimData.dealerEmail = dealerUser.email || claimData.dealerEmail;
@ -1148,14 +1180,15 @@ export class DealerClaimService {
// Fallback 2: Handle cases where activity is found but taxationType is missing, or activity not found
if (activity && activity.taxationType) {
serializedClaimDetails.defaultGstRate = Number(activity.gstRate) || 18;
const isNonGstStatus = activity.taxationType.toLowerCase().includes('non');
serializedClaimDetails.defaultGstRate = isNonGstStatus ? 0 : 18;
serializedClaimDetails.taxationType = activity.taxationType;
logger.info(`[DealerClaimService] Resolved from ActivityType record: ${activity.taxationType}`);
} else {
// Infer from title if record is missing or incomplete
const isNonGst = activityTypeTitle.toLowerCase().includes('non');
serializedClaimDetails.taxationType = isNonGst ? 'Non GST' : 'GST';
serializedClaimDetails.defaultGstRate = isNonGst ? 0 : (activity ? (Number(activity.gstRate) || 18) : 18);
serializedClaimDetails.defaultGstRate = isNonGst ? 0 : 18;
logger.info(`[DealerClaimService] Inferred taxationType from title: ${serializedClaimDetails.taxationType} (Activity record ${activity ? 'found but missing taxationType' : 'not found'})`);
}
@ -3638,7 +3671,7 @@ export class DealerClaimService {
TRNS_UNIQ_NO: item.transactionCode || '',
CLAIM_NUMBER: requestNumber,
INV_NUMBER: invoice.invoiceNumber || '',
DEALER_CODE: claimDetails.dealerCode,
DEALER_CODE: padDealerCode(claimDetails.dealerCode),
IO_NUMBER: internalOrder?.ioNumber || '',
CLAIM_DOC_TYP: sapRefNo,
CLAIM_TYPE: claimDetails.activityType,
@ -3655,7 +3688,7 @@ export class DealerClaimService {
return row;
});
await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${claimDetails.dealerCode}_${requestNumber}.csv`, isNonGst);
await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${padDealerCode(claimDetails.dealerCode)}_${requestNumber}.csv`, isNonGst);
await invoice.update({
wfmPushStatus: 'SUCCESS',

View File

@ -25,7 +25,7 @@ import { Form16NonSubmittedNotification } from '../models/Form16NonSubmittedNoti
import { Dealer } from '../models/Dealer';
import { User } from '../models/User';
import { Priority, WorkflowStatus } from '../types/common.types';
import { generateRequestNumber } from '../utils/helpers';
import { generateRequestNumber, padDealerCode } from '../utils/helpers';
import { gcsStorageService } from './gcsStorage.service';
import { activityService } from './activity.service';
import { wfmFileService } from './wfmFile.service';
@ -415,6 +415,44 @@ function form16FyCompact(financialYear: string): string {
return fy;
}
/** FY Short for 16-char ID: "2025-26" -> "26" */
function form16FyShort(financialYear: string): string {
const fy = normalizeFinancialYear(financialYear) || (financialYear || '').trim();
const m = fy.match(/-(\d{2})$/);
return m ? m[1] : (fy.length >= 2 ? fy.slice(-2) : 'XX');
}
/** Get next sequence for Form 16 credit note (4 digits) */
async function getNextForm16NoteSequence(
dealerCode: string,
financialYear: string,
quarter: string
): Promise<string> {
const dc = padDealerCode(dealerCode);
const fy = form16FyShort(financialYear);
const q = normalizeQuarter(quarter);
const prefix = `CN${dc}${fy}${q}`;
const lastNote = (await Form16CreditNote.findOne({
where: {
creditNoteNumber: { [Op.like]: `${prefix}%` },
},
order: [['creditNoteNumber', 'DESC']],
attributes: ['creditNoteNumber'],
})) as any;
let seq = 1;
if (lastNote?.creditNoteNumber) {
const lastSeqStr = lastNote.creditNoteNumber.slice(-4);
const lastSeq = parseInt(lastSeqStr, 10);
if (!isNaN(lastSeq)) {
seq = lastSeq + 1;
}
}
return seq.toString().padStart(4, '0');
}
/**
* Sanitize certificate number for use in note numbers (alphanumeric and single hyphens only).
*/
@ -424,41 +462,32 @@ function sanitizeCertificateNumber(raw: string): string {
}
/**
* Form 16 credit note number: CN-F-16-{certificateNumber}-{dealerCode}-{FY}-{quarter}-V{version}
* Supports revised 26AS / Form 16 resubmission versioning.
* Form 16 credit note number (16 chars): CN{dc}{fy}{q}{seq}
* Example: CN00628226Q20001
*/
export function formatForm16CreditNoteNumber(
export async function formatForm16CreditNoteNumber(
dealerCode: string,
financialYear: string,
quarter: string,
certificateNumber: string,
version: number = 1
): string {
const cert = sanitizeCertificateNumber(certificateNumber);
const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX';
const fy = form16FyCompact(financialYear) || 'XX';
const q = normalizeQuarter(quarter) || 'X';
const v = Math.max(1, Math.floor(version));
return `CN-F-16-${cert}-${dc}-${fy}-${q}-V${v}`;
quarter: string
): Promise<string> {
const dc = padDealerCode(dealerCode);
const fy = form16FyShort(financialYear);
const q = normalizeQuarter(quarter);
const seq = await getNextForm16NoteSequence(dealerCode, financialYear, quarter);
return `CN${dc}${fy}${q}${seq}`;
}
/**
* Form 16 debit note number: DN-F-16-{creditNoteCertificateNumber}-{dc}-{fy}-{q}-V{version}
* Uses the certificate number of the credit note being reversed (same Form 16A certificate that led to that credit note).
* Form 16 debit note number (16 chars): DN{dc}{fy}{q}{seq}
* Usually matches the sequence of the credit note being reversed.
*/
export function formatForm16DebitNoteNumber(
dealerCode: string,
financialYear: string,
quarter: string,
version: number = 1,
creditNoteCertificateNumber: string = ''
creditNoteNumber: string
): string {
const cert = sanitizeCertificateNumber(creditNoteCertificateNumber) || 'XX';
const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX';
const fy = form16FyCompact(financialYear) || 'XX';
const q = normalizeQuarter(quarter) || 'X';
const v = Math.max(1, Math.floor(version));
return `DN-F-16-${cert}-${dc}-${fy}-${q}-V${v}`;
if (creditNoteNumber.startsWith('CN')) {
return creditNoteNumber.replace(/^CN/, 'DN');
}
return creditNoteNumber.replace(/^.{2}/, 'DN'); // fallback
}
/**
@ -550,7 +579,7 @@ async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise
const dealerCode = (sub.dealerCode || '').toString().trim();
const certificateNumber = (sub.form16aNumber || '').toString().trim();
const version = typeof sub.version === 'number' && sub.version >= 1 ? sub.version : 1;
const cnNumber = formatForm16CreditNoteNumber(dealerCode, financialYear, quarter, certificateNumber, version);
const cnNumber = await formatForm16CreditNoteNumber(dealerCode, financialYear, quarter);
const now = new Date();
const creditNote = await Form16CreditNote.create({
submissionId: submission.id,
@ -588,7 +617,7 @@ async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise
const csvRow: Record<string, string | number> = {
TRNS_UNIQ_NO: trnsUniqNo,
TDS_TRNS_ID: cnNumber,
DEALER_CODE: dealerCode,
DEALER_CODE: padDealerCode(dealerCode),
TDS_TRNS_DOC_TYP: 'ZTDS',
DLR_TAN_NO: tanNumber,
'FIN_YEAR & QUARTER': finYearAndQuarter,
@ -2005,7 +2034,8 @@ export async function process26asUploadAggregation(uploadLogId: number): Promise
const creditNoteCertNumber = submission ? ((submission as any).form16aNumber || '').toString().trim() : '';
const cnFy = (creditNote as any).financialYear || fy;
const cnQuarter = (creditNote as any).quarter || q;
const debitNum = formatForm16DebitNoteNumber(dealerCode || 'XX', cnFy, cnQuarter, version, creditNoteCertNumber);
const creditNoteNumber = (creditNote as any).creditNoteNumber || (creditNote as any).credit_note_number || '';
const debitNum = formatForm16DebitNoteNumber(creditNoteNumber);
const now = new Date();
const debit = await Form16DebitNote.create({
creditNoteId: creditNote.id,
@ -2036,8 +2066,8 @@ export async function process26asUploadAggregation(uploadLogId: number): Promise
const finYearAndQuarter = fyCompact && cnQuarter ? `FY ${fyCompact}_${cnQuarter}` : '';
const csvRow: Record<string, string | number> = {
TRNS_UNIQ_NO: trnsUniqNo,
TDS_TRNS_ID: debitNum,
DEALER_CODE: dealerCode || 'XX',
TDS_TRNS_ID: creditNoteNumber,
DEALER_CODE: padDealerCode(dealerCode),
TDS_TRNS_DOC_TYP: 'ZTDS',
'Org.Document Number': debit.id,
DLR_TAN_NO: tanNumber,

View File

@ -33,11 +33,6 @@ export class PWCIntegrationService {
* Resolve GL Code based on Activity Type and Internal Order
*/
private async resolveGLCode(activityTypeId: string, ioNumber?: string): Promise<string> {
const activity = await ActivityType.findByPk(activityTypeId);
if (activity?.glCode) {
return activity.glCode;
}
// Default Fallback or IO based logic if required
// Based on "IO GL will be changed" comment in user screenshot
if (ioNumber) {
@ -148,9 +143,9 @@ export class PWCIntegrationService {
const groupedExpenses: Record<string, any> = {};
expenses.forEach((expense: any) => {
const hsnCd = expense.hsnCode || activity.hsnCode || activity.sacCode || "998311";
const hsnCd = expense.hsnCode || "998311";
const gstRate = (expense.gstRate === undefined || expense.gstRate === null || Number(expense.gstRate) === 0)
? Number(activity.gstRate || 18)
? 18
: Number(expense.gstRate);
const groupKey = `${hsnCd}_${gstRate}`;
@ -288,7 +283,7 @@ export class PWCIntegrationService {
});
} else {
// Fallback to single line item if no expenses found
const gstRate = isNonGSTActivity ? 0 : Number(activity.gstRate || 18);
const gstRate = isNonGSTActivity ? 0 : 18;
const assAmt = finalAmount;
let igstAmt = 0, cgstAmt = 0, sgstAmt = 0;
@ -309,7 +304,7 @@ export class PWCIntegrationService {
const slNo = 1;
const fallbackHsn = activity.hsnCode || activity.sacCode || "998311";
const fallbackHsn = "998311";
const fallbackIsService = this.isServiceHSN(fallbackHsn) === "Y";
const transactionCode = `${customInvoiceNumber}-${String(slNo).padStart(2, '0')}`;

View File

@ -158,7 +158,7 @@ export class WFMFileService {
* Get credit note details from outgoing CSV
*/
async getCreditNoteDetails(dealerCode: string, requestNumber: string, isNonGst: boolean = false): Promise<any[]> {
const fileName = `CN_${dealerCode}_${requestNumber}.csv`;
const fileName = `CN_${String(dealerCode).padStart(6, '0')}_${requestNumber}.csv`;
const filePath = this.getOutgoingPath(fileName, isNonGst);
try {

View File

@ -106,3 +106,12 @@ export const generateChecksum = (data: string): string => {
export const sleep = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* Pad dealer code to 6 digits with leading zeros
* Example: '73' -> '000073'
*/
export const padDealerCode = (dealerCode: string | number): string => {
if (dealerCode === null || dealerCode === undefined) return '';
return String(dealerCode).padStart(6, '0');
};

View File

@ -58,6 +58,9 @@ export const createActivityTypeSchema = z.object({
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'),
creditPostingOn: z.enum(['Spares', 'Vehicle', 'GMA', 'Apparel'], {
errorMap: () => ({ message: 'Credit posting on must be Spares, Vehicle, GMA or Apparel' }),
}),
});
export const updateActivityTypeSchema = createActivityTypeSchema.partial();