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:
parent
64e8c2237a
commit
0f99fe68d5
@ -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
1
build/assets/index-D2NzWWdB.css
Normal file
1
build/assets/index-D2NzWWdB.css
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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 || '';
|
||||
|
||||
86
src/migrations/20260317-refactor-activity-types-columns.ts
Normal file
86
src/migrations/20260317-refactor-activity-types-columns.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 },
|
||||
|
||||
];
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')}`;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user