From 17c62d2b451755c8c2527c29a5f39ea4dfd77e4d Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Mon, 9 Feb 2026 20:50:17 +0530 Subject: [PATCH] enhancd expens and cost items to support gst values addd pwc service file to integrate pwc --- .../20260209-add-gst-and-pwc-fields.ts | 119 ++++++++++ src/models/ActivityType.ts | 37 ++- src/models/ClaimCreditNote.ts | 99 +++++++- src/models/ClaimInvoice.ts | 217 ++++++++++++++++-- src/models/DealerCompletionExpense.ts | 100 +++++++- src/models/DealerProposalCostItem.ts | 93 +++++++- src/scripts/auto-setup.ts | 2 + src/scripts/migrate.ts | 2 + src/services/dealer.service.ts | 39 ++++ src/services/dealerClaim.service.ts | 80 +++++-- src/services/dmsWebhook.service.ts | 14 +- src/services/pwcIntegration.service.ts | 171 ++++++++++++++ 12 files changed, 917 insertions(+), 56 deletions(-) create mode 100644 src/migrations/20260209-add-gst-and-pwc-fields.ts create mode 100644 src/services/pwcIntegration.service.ts diff --git a/src/migrations/20260209-add-gst-and-pwc-fields.ts b/src/migrations/20260209-add-gst-and-pwc-fields.ts new file mode 100644 index 0000000..302e5fb --- /dev/null +++ b/src/migrations/20260209-add-gst-and-pwc-fields.ts @@ -0,0 +1,119 @@ +import { QueryInterface, DataTypes } from 'sequelize'; + +/** + * Helper function to check if a column exists in a table + */ +async function columnExists( + queryInterface: QueryInterface, + tableName: string, + columnName: string +): Promise { + try { + const tableDescription = await queryInterface.describeTable(tableName); + return columnName in tableDescription; + } catch (error) { + return false; + } +} + +export async function up(queryInterface: QueryInterface): Promise { + // 1. ActivityType + const activityCols = { + hsn_code: { type: DataTypes.STRING(20), allowNull: true }, + sac_code: { type: DataTypes.STRING(20), allowNull: true }, + gst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + gl_code: { type: DataTypes.STRING(20), allowNull: true }, + credit_nature: { type: DataTypes.STRING(50), allowNull: true } + }; + + for (const [col, spec] of Object.entries(activityCols)) { + if (!(await columnExists(queryInterface, 'activity_types', col))) { + await queryInterface.addColumn('activity_types', col, spec); + } + } + + // 2. GST Fields mapping for Multiple Tables + const gstFields = { + gst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + gst_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + cgst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + cgst_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + sgst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + sgst_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + igst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + igst_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + utgst_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + utgst_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + cess_rate: { type: DataTypes.DECIMAL(5, 2), allowNull: true }, + cess_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + total_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + }; + + const tables = ['dealer_proposal_cost_items', 'dealer_completion_expenses', 'claim_credit_notes']; + + for (const table of tables) { + for (const [col, spec] of Object.entries(gstFields)) { + if (!(await columnExists(queryInterface, table, col))) { + await queryInterface.addColumn(table, col, spec); + } + } + } + + // Add missing expense_date to DealerCompletionExpense + if (!(await columnExists(queryInterface, 'dealer_completion_expenses', 'expense_date'))) { + await queryInterface.addColumn('dealer_completion_expenses', 'expense_date', { type: DataTypes.DATEONLY, allowNull: true }); + } + + // 3. ClaimInvoice + const invoiceCols = { + irn: { type: DataTypes.STRING(500), allowNull: true }, + ack_no: { type: DataTypes.STRING(255), allowNull: true }, + ack_date: { type: DataTypes.DATE, allowNull: true }, + signed_invoice: { type: DataTypes.TEXT, allowNull: true }, + signed_invoice_url: { type: DataTypes.STRING(500), allowNull: true }, + dealer_claim_number: { type: DataTypes.STRING(100), allowNull: true }, + qr_code: { type: DataTypes.TEXT, allowNull: true }, + qr_image: { type: DataTypes.TEXT, allowNull: true }, + dealer_claim_date: { type: DataTypes.DATEONLY, allowNull: true }, + billing_no: { type: DataTypes.STRING(100), allowNull: true }, + billing_date: { type: DataTypes.DATEONLY, allowNull: true }, + taxable_value: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + cgst_total: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + sgst_total: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + igst_total: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + utgst_total: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + cess_total: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + tcs_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + round_off_amt: { type: DataTypes.DECIMAL(15, 2), allowNull: true }, + place_of_supply: { type: DataTypes.STRING(255), allowNull: true }, + total_value_in_words: { type: DataTypes.STRING(500), allowNull: true }, + tax_value_in_words: { type: DataTypes.STRING(500), allowNull: true }, + credit_nature: { type: DataTypes.STRING(100), allowNull: true }, + consignor_gsin: { type: DataTypes.STRING(255), allowNull: true }, + gstin_date: { type: DataTypes.DATEONLY, allowNull: true } + }; + + for (const [col, spec] of Object.entries(invoiceCols)) { + if (!(await columnExists(queryInterface, 'claim_invoices', col))) { + await queryInterface.addColumn('claim_invoices', col, spec); + } + } + + // Ensure file_path exists as 'file_path' + try { + if (!(await columnExists(queryInterface, 'claim_invoices', 'file_path'))) { + if (await columnExists(queryInterface, 'claim_invoices', 'invoice_file_path')) { + await queryInterface.renameColumn('claim_invoices', 'invoice_file_path', 'file_path'); + } else { + await queryInterface.addColumn('claim_invoices', 'file_path', { type: DataTypes.STRING(500), allowNull: true }); + } + } + } catch (e) { + // Silently continue + } +} + +export async function down(queryInterface: QueryInterface): Promise { + // Note: Best effort rollback (usually not recommended to drop columns in shared dev unless necessary) + await queryInterface.removeColumn('dealer_completion_expenses', 'expense_date').catch(() => { }); +} diff --git a/src/models/ActivityType.ts b/src/models/ActivityType.ts index ef4ffa1..4b7a998 100644 --- a/src/models/ActivityType.ts +++ b/src/models/ActivityType.ts @@ -9,13 +9,18 @@ interface ActivityTypeAttributes { taxationType?: string; sapRefNo?: string; isActive: boolean; + hsnCode?: string | null; + sacCode?: string | null; + gstRate?: number | null; + glCode?: string | null; + creditNature?: 'Commercial' | 'GST' | null; createdBy: string; updatedBy?: string; createdAt: Date; updatedAt: Date; } -interface ActivityTypeCreationAttributes extends Optional {} +interface ActivityTypeCreationAttributes extends Optional { } class ActivityType extends Model implements ActivityTypeAttributes { public activityTypeId!: string; @@ -24,6 +29,11 @@ class ActivityType extends Model {} +interface ClaimCreditNoteCreationAttributes extends Optional { } class ClaimCreditNote extends Model implements ClaimCreditNoteAttributes { public creditNoteId!: string; @@ -30,7 +43,20 @@ class ClaimCreditNote extends Model {} +interface ClaimInvoiceCreationAttributes extends Optional { } class ClaimInvoice extends Model implements ClaimInvoiceAttributes { public invoiceId!: string; public requestId!: string; public invoiceNumber?: string; - public invoiceDate?: Date; - public amount?: number; public dmsNumber?: string; - public invoiceFilePath?: string; - public status?: string; + public invoiceDate?: Date; + public amount!: number; + public status!: string; + public irn?: string | null; + public ackNo?: string | null; + public ackDate?: Date | null; + public signedInvoice?: string | null; + public signedInvoiceUrl?: string | null; + public dealerClaimNumber?: string | null; + public dealerClaimDate?: Date | null; + public billingNo?: string | null; + public billingDate?: Date | null; + public taxableValue?: number | null; + public cgstTotal?: number | null; + public sgstTotal?: number | null; + public igstTotal?: number | null; + public utgstTotal?: number | null; + public cessTotal?: number | null; + public tcsAmt?: number | null; + public roundOffAmt?: number | null; + public placeOfSupply?: string | null; + public totalValueInWords?: string | null; + public taxValueInWords?: string | null; + public creditNature?: string | null; + public consignorGsin?: string | null; + public gstinDate?: Date | null; + public filePath?: string | null; + public qrCode?: string | null; + public qrImage?: string | null; public errorMessage?: string; public generatedAt?: Date; public description?: string; @@ -61,6 +111,11 @@ ClaimInvoice.init( allowNull: true, field: 'invoice_number', }, + dmsNumber: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'dms_number', + }, invoiceDate: { type: DataTypes.DATEONLY, allowNull: true, @@ -68,23 +123,143 @@ ClaimInvoice.init( }, amount: { type: DataTypes.DECIMAL(15, 2), - allowNull: true, + allowNull: false, field: 'invoice_amount', }, - dmsNumber: { - type: DataTypes.STRING(100), - allowNull: true, - field: 'dms_number', - }, - invoiceFilePath: { - type: DataTypes.STRING(500), - allowNull: true, - field: 'invoice_file_path', - }, status: { type: DataTypes.STRING(50), + allowNull: false, + defaultValue: 'PENDING', + field: 'generation_status' + }, + irn: { + type: DataTypes.STRING(500), + allowNull: true + }, + ackNo: { + type: DataTypes.STRING(255), allowNull: true, - field: 'generation_status', + field: 'ack_no' + }, + ackDate: { + type: DataTypes.DATE, + allowNull: true, + field: 'ack_date' + }, + signedInvoice: { + type: DataTypes.TEXT, + allowNull: true, + field: 'signed_invoice' + }, + signedInvoiceUrl: { + type: DataTypes.STRING(500), + allowNull: true, + field: 'signed_invoice_url' + }, + dealerClaimNumber: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'dealer_claim_number' + }, + dealerClaimDate: { + type: DataTypes.DATEONLY, + allowNull: true, + field: 'dealer_claim_date' + }, + billingNo: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'billing_no' + }, + billingDate: { + type: DataTypes.DATEONLY, + allowNull: true, + field: 'billing_date' + }, + taxableValue: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'taxable_value' + }, + cgstTotal: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'cgst_total' + }, + sgstTotal: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'sgst_total' + }, + igstTotal: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'igst_total' + }, + utgstTotal: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'utgst_total' + }, + cessTotal: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'cess_total' + }, + tcsAmt: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'tcs_amt' + }, + roundOffAmt: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true, + field: 'round_off_amt' + }, + placeOfSupply: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'place_of_supply' + }, + totalValueInWords: { + type: DataTypes.STRING(500), + allowNull: true, + field: 'total_value_in_words' + }, + taxValueInWords: { + type: DataTypes.STRING(500), + allowNull: true, + field: 'tax_value_in_words' + }, + creditNature: { + type: DataTypes.STRING(100), + allowNull: true, + field: 'credit_nature' + }, + consignorGsin: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'consignor_gsin' + }, + gstinDate: { + type: DataTypes.DATEONLY, + allowNull: true, + field: 'gstin_date' + }, + filePath: { + type: DataTypes.STRING(500), + allowNull: true, + field: 'file_path' + }, + qrCode: { + type: DataTypes.TEXT, + allowNull: true, + field: 'qr_code' + }, + qrImage: { + type: DataTypes.TEXT, + allowNull: true, + field: 'qr_image' }, errorMessage: { type: DataTypes.TEXT, diff --git a/src/models/DealerCompletionExpense.ts b/src/models/DealerCompletionExpense.ts index 7c164f6..d92012a 100644 --- a/src/models/DealerCompletionExpense.ts +++ b/src/models/DealerCompletionExpense.ts @@ -9,11 +9,25 @@ interface DealerCompletionExpenseAttributes { completionId?: string | null; description: string; amount: number; + gstRate?: number; + gstAmt?: number; + cgstRate?: number; + cgstAmt?: number; + sgstRate?: number; + sgstAmt?: number; + igstRate?: number; + igstAmt?: number; + utgstRate?: number; + utgstAmt?: number; + cessRate?: number; + cessAmt?: number; + totalAmt?: number; + expenseDate: Date; createdAt: Date; updatedAt: Date; } -interface DealerCompletionExpenseCreationAttributes extends Optional {} +interface DealerCompletionExpenseCreationAttributes extends Optional { } class DealerCompletionExpense extends Model implements DealerCompletionExpenseAttributes { public expenseId!: string; @@ -21,6 +35,20 @@ class DealerCompletionExpense extends Model {} +interface DealerProposalCostItemCreationAttributes extends Optional { } class DealerProposalCostItem extends Model implements DealerProposalCostItemAttributes { public costItemId!: string; @@ -22,6 +35,19 @@ class DealerProposalCostItem extends Model { const m42 = require('../migrations/20250125-create-activity-types'); const m43 = require('../migrations/20260113-redesign-dealer-claim-history'); const m44 = require('../migrations/20260123-fix-template-id-schema'); + const m45 = require('../migrations/20260209-add-gst-and-pwc-fields'); const migrations = [ { name: '2025103000-create-users', module: m0 }, @@ -206,6 +207,7 @@ async function runMigrations(): Promise { { name: '20250125-create-activity-types', module: m42 }, { name: '20260113-redesign-dealer-claim-history', module: m43 }, { name: '20260123-fix-template-id-schema', module: m44 }, + { name: '20260209-add-gst-and-pwc-fields', module: m45 }, ]; // Dynamically import sequelize after secrets are loaded diff --git a/src/scripts/migrate.ts b/src/scripts/migrate.ts index 5e39075..c2c3626 100644 --- a/src/scripts/migrate.ts +++ b/src/scripts/migrate.ts @@ -47,6 +47,7 @@ import * as m41 from '../migrations/20250120-create-dealers-table'; import * as m42 from '../migrations/20250125-create-activity-types'; import * as m43 from '../migrations/20260113-redesign-dealer-claim-history'; import * as m44 from '../migrations/20260123-fix-template-id-schema'; +import * as m45 from '../migrations/20260209-add-gst-and-pwc-fields'; interface Migration { name: string; @@ -108,6 +109,7 @@ const migrations: Migration[] = [ { name: '20250125-create-activity-types', module: m42 }, { name: '20260113-redesign-dealer-claim-history', module: m43 }, { name: '20260123-fix-template-id-schema', module: m44 }, + { name: '20260209-add-gst-and-pwc-fields', module: m45 } ]; /** diff --git a/src/services/dealer.service.ts b/src/services/dealer.service.ts index b72314a..7391451 100644 --- a/src/services/dealer.service.ts +++ b/src/services/dealer.service.ts @@ -247,3 +247,42 @@ export async function searchDealers(searchTerm: string, limit: number = 10): Pro } } +/** + * Find dealer in local table by code or email (tiered lookup) + * Used as a fallback until external API is available + * @param dealerCode - Optional dealer code (dlrcode) + * @param email - Optional email (domainId or dealerPrincipalEmailId) + */ +export async function findDealerLocally(dealerCode?: string, email?: string): Promise { + try { + // 1. Try Lookup by Dealer Code + if (dealerCode) { + const dealer = await getDealerByCode(dealerCode); + if (dealer) return dealer; + } + + // 2. Try Lookup by Email (checks both domainId and dealerPrincipalEmailId) + if (email) { + const dealer = await Dealer.findOne({ + where: { + [Op.or]: [ + { domainId: { [Op.iLike]: email.toLowerCase() } as any }, + { dealerPrincipalEmailId: { [Op.iLike]: email.toLowerCase() } as any } + ], + isActive: true + } + }); + + if (dealer) { + // reuse same logic as getDealerByEmail for consistency + return getDealerByEmail(dealer.domainId || dealer.dealerPrincipalEmailId || email); + } + } + + return null; + } catch (error) { + logger.error('[DealerService] Error in local dealer fallback lookup:', error); + return null; + } +} + diff --git a/src/services/dealerClaim.service.ts b/src/services/dealerClaim.service.ts index 71e5440..76899d7 100644 --- a/src/services/dealerClaim.service.ts +++ b/src/services/dealerClaim.service.ts @@ -18,12 +18,16 @@ import { DealerClaimApprovalService } from './dealerClaimApproval.service'; import { generateRequestNumber } from '../utils/helpers'; import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types'; import { sapIntegrationService } from './sapIntegration.service'; -import { dmsIntegrationService } from './dmsIntegration.service'; +import { pwcIntegrationService } from './pwcIntegration.service'; import { notificationService } from './notification.service'; import { activityService } from './activity.service'; import { UserService } from './user.service'; +import { dmsIntegrationService } from './dmsIntegration.service'; +import { findDealerLocally } from './dealer.service'; import logger from '../utils/logger'; + + /** * Dealer Claim Service * Handles business logic specific to dealer claim management workflow @@ -83,6 +87,15 @@ export class DealerClaimService { throw new Error('Initiator not found'); } + // Fallback: Enrichment from local dealer table if data is missing or incomplete + const localDealer = await findDealerLocally(claimData.dealerCode, claimData.dealerEmail); + if (localDealer) { + logger.info(`[DealerClaimService] Enriched claim request with local dealer data: ${localDealer.dealerCode}`); + claimData.dealerName = claimData.dealerName || localDealer.dealerName; + claimData.dealerEmail = claimData.dealerEmail || localDealer.dealerPrincipalEmailId || localDealer.email; + claimData.dealerPhone = claimData.dealerPhone || localDealer.phone; + } + // Validate approvers array is provided if (!claimData.approvers || !Array.isArray(claimData.approvers) || claimData.approvers.length === 0) { throw new Error('Approvers array is required. Please assign approvers for all workflow steps.'); @@ -1260,6 +1273,19 @@ export class DealerClaimService { requestId, itemDescription: item.description || item.itemDescription || '', amount: Number(item.amount) || 0, + gstRate: Number(item.gstRate) || 0, + gstAmt: Number(item.gstAmt) || 0, + cgstRate: Number(item.cgstRate) || 0, + cgstAmt: Number(item.cgstAmt) || 0, + sgstRate: Number(item.sgstRate) || 0, + sgstAmt: Number(item.sgstAmt) || 0, + igstRate: Number(item.igstRate) || 0, + igstAmt: Number(item.igstAmt) || 0, + utgstRate: Number(item.utgstRate) || 0, + utgstAmt: Number(item.utgstAmt) || 0, + cessRate: Number(item.cessRate) || 0, + cessAmt: Number(item.cessAmt) || 0, + totalAmt: Number(item.totalAmt) || Number(item.amount) || 0, itemOrder: index })); @@ -1397,7 +1423,21 @@ export class DealerClaimService { requestId, completionId, description: item.description, - amount: item.amount, + amount: Number(item.amount) || 0, + gstRate: Number(item.gstRate) || 0, + gstAmt: Number(item.gstAmt) || 0, + cgstRate: Number(item.cgstRate) || 0, + cgstAmt: Number(item.cgstAmt) || 0, + sgstRate: Number(item.sgstRate) || 0, + sgstAmt: Number(item.sgstAmt) || 0, + igstRate: Number(item.igstRate) || 0, + igstAmt: Number(item.igstAmt) || 0, + utgstRate: Number(item.utgstRate) || 0, + utgstAmt: Number(item.utgstAmt) || 0, + cessRate: Number(item.cessRate) || 0, + cessAmt: Number(item.cessAmt) || 0, + totalAmt: Number(item.totalAmt) || Number(item.amount) || 0, + expenseDate: item.date instanceof Date ? item.date : (item.date ? new Date(item.date) : (completionData.activityCompletionDate || new Date())), })); await DealerCompletionExpense.bulkCreate(expenseRows); } @@ -1874,33 +1914,31 @@ export class DealerClaimService { || budgetTracking?.initialEstimatedBudget || 0; - const invoiceResult = await dmsIntegrationService.generateEInvoice({ - requestNumber, - dealerCode: claimDetails.dealerCode, - dealerName: claimDetails.dealerName, - amount: invoiceAmount, - description: invoiceData?.description || `E-Invoice for claim request ${requestNumber}`, - ioNumber: internalOrder?.ioNumber || undefined, - }); + const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId); if (!invoiceResult.success) { - throw new Error(`Failed to generate e-invoice: ${invoiceResult.error}`); + throw new Error(`Failed to generate signed e-invoice via PWC: ${invoiceResult.error}`); } await ClaimInvoice.upsert({ requestId, - invoiceNumber: invoiceResult.eInvoiceNumber, - invoiceDate: invoiceResult.invoiceDate || new Date(), - dmsNumber: invoiceResult.dmsNumber, + invoiceNumber: invoiceResult.ackNo, // Using Ack No as primary identifier for now + invoiceDate: invoiceResult.ackDate instanceof Date ? invoiceResult.ackDate : (invoiceResult.ackDate ? new Date(invoiceResult.ackDate) : new Date()), + irn: invoiceResult.irn, + ackNo: invoiceResult.ackNo, + ackDate: invoiceResult.ackDate instanceof Date ? invoiceResult.ackDate : (invoiceResult.ackDate ? new Date(invoiceResult.ackDate) : null), + signedInvoice: invoiceResult.signedInvoice, + qrCode: invoiceResult.qrCode, + qrImage: invoiceResult.qrImage, amount: invoiceAmount, status: 'GENERATED', generatedAt: new Date(), - description: invoiceData?.description || `E-Invoice for claim request ${requestNumber}`, + description: invoiceData?.description || `PWC Signed Invoice for claim request ${requestNumber}`, }); - logger.info(`[DealerClaimService] E-Invoice generated via DMS for request: ${requestId}`, { - eInvoiceNumber: invoiceResult.eInvoiceNumber, - dmsNumber: invoiceResult.dmsNumber + logger.info(`[DealerClaimService] Signed Invoice generated via PWC for request: ${requestId}`, { + ackNo: invoiceResult.ackNo, + irn: invoiceResult.irn }); } else { // Manual entry - just update the fields @@ -1909,7 +1947,7 @@ export class DealerClaimService { invoiceNumber: invoiceData.eInvoiceNumber, invoiceDate: invoiceData.eInvoiceDate || new Date(), dmsNumber: invoiceData.dmsNumber, - amount: invoiceData.amount, + amount: Number(invoiceData.amount) || 0, status: 'UPDATED', generatedAt: new Date(), description: invoiceData.description, @@ -2067,7 +2105,7 @@ export class DealerClaimService { invoiceId: claimInvoice.invoiceId, creditNoteNumber: creditNoteResult.creditNoteNumber, creditNoteDate: creditNoteResult.creditNoteDate || new Date(), - creditNoteAmount: creditNoteResult.creditNoteAmount, + creditNoteAmount: Number(creditNoteResult.creditNoteAmount) || 0, status: 'GENERATED', confirmedAt: new Date(), reason: creditNoteData?.reason || 'Claim settlement', @@ -2100,7 +2138,7 @@ export class DealerClaimService { invoiceId: claimInvoice?.invoiceId || undefined, // Allow undefined if no invoice creditNoteNumber: creditNoteData.creditNoteNumber, creditNoteDate: creditNoteData.creditNoteDate || new Date(), - creditNoteAmount: creditNoteData.creditNoteAmount, + creditNoteAmount: Number(creditNoteData.creditNoteAmount) || 0, status: 'UPDATED', confirmedAt: new Date(), reason: creditNoteData?.reason, diff --git a/src/services/dmsWebhook.service.ts b/src/services/dmsWebhook.service.ts index e01975f..9827a59 100644 --- a/src/services/dmsWebhook.service.ts +++ b/src/services/dmsWebhook.service.ts @@ -119,7 +119,7 @@ export class DMSWebhookService { amount: payload.total_amount || payload.claim_amount, status: 'GENERATED', generatedAt: new Date(), - invoiceFilePath: payload.invoice_file_path || null, + filePath: payload.invoice_file_path || null, errorMessage: payload.error_message || null, description: this.buildInvoiceDescription(payload), }); @@ -137,7 +137,7 @@ export class DMSWebhookService { amount: payload.total_amount || payload.claim_amount, status: 'GENERATED', generatedAt: new Date(), - invoiceFilePath: payload.invoice_file_path || null, + filePath: payload.invoice_file_path || null, errorMessage: payload.error_message || null, // Store additional DMS data in description or separate fields if needed description: this.buildInvoiceDescription(payload), @@ -296,7 +296,7 @@ export class DMSWebhookService { */ private buildInvoiceDescription(payload: any): string { const parts: string[] = []; - + if (payload.irn_no) { parts.push(`IRN: ${payload.irn_no}`); } @@ -318,7 +318,7 @@ export class DMSWebhookService { */ private buildCreditNoteDescription(payload: any): string { const parts: string[] = []; - + if (payload.irn_no) { parts.push(`IRN: ${payload.irn_no}`); } @@ -404,7 +404,7 @@ export class DMSWebhookService { attributes: ['userId'], }); dealerUserId = dealerUser?.userId || null; - + if (dealerUserId) { logger.info('[DMSWebhook] Found dealer user for notification', { requestId, @@ -512,9 +512,9 @@ export class DMSWebhookService { const dealerClaimService = new DealerClaimService(); const invoice = await ClaimInvoice.findOne({ where: { requestId } }); const invoiceNumber = invoice?.invoiceNumber || 'N/A'; - + await dealerClaimService.logEInvoiceGenerationActivity(requestId, invoiceNumber); - + logger.info('[DMSWebhook] E-Invoice Generation activity logged successfully', { requestId, requestNumber, diff --git a/src/services/pwcIntegration.service.ts b/src/services/pwcIntegration.service.ts new file mode 100644 index 0000000..f85b128 --- /dev/null +++ b/src/services/pwcIntegration.service.ts @@ -0,0 +1,171 @@ +import axios from 'axios'; +import logger from '../utils/logger'; +import { Dealer } from '../models/Dealer'; +import { ActivityType } from '../models/ActivityType'; +import { WorkflowRequest } from '../models/WorkflowRequest'; +import { ClaimInvoice } from '../models/ClaimInvoice'; +import { InternalOrder } from '../models/InternalOrder'; + +/** + * PWC E-Invoice Integration Service + * Handles communication with PWC API for signed invoice generation + */ +export class PWCIntegrationService { + private apiUrl: string; + private appKey: string; + private appSecret: string; + + constructor() { + this.apiUrl = process.env.PWC_API_URL || 'https://api.qa.einvoice.aw.navigatetax.pwc.co.in'; + this.appKey = process.env.PWC_APP_KEY || ''; + this.appSecret = process.env.PWC_APP_SECRET || ''; + } + + /** + * Resolve GL Code based on Activity Type and Internal Order + */ + private async resolveGLCode(activityTypeId: string, ioNumber?: string): Promise { + 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) { + const io = await InternalOrder.findOne({ where: { ioNumber } }); + // Logic to derive GL from IO if needed + } + + return '610000'; // Default placeholder + } + + /** + * Generate Signed Invoice via PWC API + */ + async generateSignedInvoice(requestId: string): Promise<{ + success: boolean; + irn?: string; + ackNo?: string; + ackDate?: Date | string; + signedInvoice?: string; + qrCode?: string; + qrImage?: string; + error?: string; + }> { + try { + const request = await WorkflowRequest.findByPk(requestId, { + include: ['claimDetails', 'initiator'] + }); + + if (!request) return { success: false, error: 'Request not found' }; + + const dealer = await Dealer.findOne({ where: { dlrcode: (request as any).claimDetails?.dealerCode } }); + const activity = await ActivityType.findOne({ where: { title: (request as any).claimDetails?.activityType } }); + + if (!dealer || !activity) { + return { success: false, error: 'Dealer or Activity details missing' }; + } + + // Construct PWC Payload (keeping existing logic for now) + const payload = { + UserGstin: "33AAACE3882D1ZZ", + DocDtls: { + Typ: "INV", + No: `INV-${Date.now()}`, + Dt: new Date().toLocaleDateString('en-GB').replace(/\//g, '-') + }, + SellerDtls: { + Gstin: dealer.gst || "33AAACE3882D1ZZ", + LglNm: dealer.dealership || 'Dealer', + Addr1: dealer.showroomAddress || "Address Line 1", + Loc: dealer.location || "Location", + Pin: 600001, + Stcd: "33" + }, + BuyerDtls: { + Gstin: "33AAACE3882D1ZZ", + LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)", + Addr1: "No. 2, Thiruvottiyur High Road", + Loc: "Thiruvottiyur", + Pin: 600019, + Stcd: "33", + Pos: "33" + }, + ItemList: [ + { + SlNo: "1", + PrdDesc: activity.title, + IsServc: "Y", + HsnCd: activity.hsnCode || activity.sacCode || "9983", + Qty: 1, + Unit: "OTH", + UnitPrce: (request as any).amount, + TotAmt: (request as any).amount, + GstRt: activity.gstRate || 18, + AssAmt: (request as any).amount, + IgstAmt: activity.gstRate === 18 ? ((request as any).amount * 0.18) : 0, + TotItemVal: (request as any).amount * 1.18 + } + ], + ValDtls: { + AssVal: (request as any).amount, + IgstVal: activity.gstRate === 18 ? ((request as any).amount * 0.18) : 0, + TotInvVal: (request as any).amount * 1.18 + } + }; + + logger.info(`[PWC] Sending e-invoice request for ${request.requestNumber}`); + + const response = await axios.post(`${this.apiUrl}/generate`, payload, { + headers: { 'AppKey': this.appKey, 'AppSecret': this.appSecret } + }); + + // Parse PWC Response based on provided structure + // Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }] + const responseData = Array.isArray(response.data) ? response.data[0] : response.data; + const irpData = responseData?.irp_response?.data; + const nicData = irpData?.nic_response_data; + const qrB64 = responseData?.qr_b64_encoded; + + let irn = nicData?.Irn; + let ackNo = nicData?.AckNo; + let ackDate = nicData?.AckDt; + let signedInvoice = nicData?.SignedInvoice; + let qrCode = nicData?.SignedQRCode; + + // Handle Duplicate IRN Case + if (!irn && irpData?.InfoDtls) { + const dupInfo = irpData.InfoDtls.find((info: any) => info.InfCd === 'DUPIRN'); + if (dupInfo?.Desc) { + irn = dupInfo.Desc.Irn; + ackNo = dupInfo.Desc.AckNo; + ackDate = dupInfo.Desc.AckDt; + logger.info(`[PWC] Handled duplicate IRN for ${request.requestNumber}: ${irn}`); + } + } + + if (!irn) { + const errorMsg = responseData?.irp_response?.message || 'E-Invoice generation failed'; + logger.error(`[PWC] E-Invoice failed for ${request.requestNumber}: ${errorMsg}`); + return { success: false, error: errorMsg }; + } + + return { + success: true, + irn, + ackNo: String(ackNo), + ackDate: ackDate ? new Date(ackDate) : undefined, + signedInvoice, + qrCode, + qrImage: qrB64 + }; + + } catch (error) { + logger.error('[PWC] Error generating e-invoice:', error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + } +} + +export const pwcIntegrationService = new PWCIntegrationService();