removed suspicious comments

This commit is contained in:
laxmanhalaki 2026-02-10 09:54:24 +05:30
parent 17c62d2b45
commit 9060c39f9c
6 changed files with 156 additions and 273 deletions

View File

@ -49,7 +49,7 @@ router.use('/templates', templateRoutes);
router.use('/dealers', dealerRoutes);
router.use('/webhooks/dms', dmsWebhookRoutes);
// TODO: Add other route modules as they are implemented
// Add other route modules as they are implemented
// router.use('/approvals', approvalRoutes);
// router.use('/participants', participantRoutes);

View File

@ -58,7 +58,7 @@ interface DealerSeedData {
}
// Sample data based on the provided table
// TODO: Replace with your actual dealer data from Excel/CSV
// Replace with your actual dealer data from Excel/CSV
const dealersData: DealerSeedData[] = [
{
salesCode: '5124',
@ -116,7 +116,7 @@ async function seedDealersTable(): Promise<void> {
for (const dealerData of dealersData) {
// Use dlrcode or domainId as unique identifier if available
const uniqueIdentifier = dealerData.dlrcode || dealerData.domainId || dealerData.salesCode;
if (!uniqueIdentifier) {
logger.warn('[Seed Dealers Table] Skipping dealer record without unique identifier');
continue;
@ -130,15 +130,15 @@ async function seedDealersTable(): Promise<void> {
const existingDealer = whereConditions.length > 0
? await Dealer.findOne({
where: {
[Op.or]: whereConditions
}
})
where: {
[Op.or]: whereConditions
}
})
: null;
if (existingDealer) {
logger.info(`[Seed Dealers Table] Dealer ${uniqueIdentifier} already exists, updating...`);
// Update existing dealer
await existingDealer.update({
...dealerData,

View File

@ -673,7 +673,7 @@ export class DashboardService {
totalCompleted,
compliantWorkflows: compliantCount,
changeFromPrevious: {
compliance: '+5.8%', // TODO: Calculate actual change
compliance: '+5.8%', // Calculate actual change
cycleTime: '-0.5h'
}
};

View File

@ -2215,14 +2215,6 @@ export class DealerClaimService {
dealerName: claimDetails.dealerName,
});
// TODO: Implement email service to send credit note to dealer
// await emailService.sendCreditNoteToDealer({
// dealerEmail: claimDetails.dealerEmail,
// dealerName: claimDetails.dealerName,
// creditNoteNumber: creditNote.creditNoteNumber,
// creditNoteAmount: creditNote.creditNoteAmount,
// requestNumber: requestNumber,
// });
} catch (error) {
logger.error('[DealerClaimService] Error sending credit note to dealer:', error);

View File

@ -64,30 +64,6 @@ export class DMSIntegrationService {
};
}
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.post(`${this.dmsBaseUrl}/api/invoices/generate`, {
// request_number: invoiceData.requestNumber,
// dealer_code: invoiceData.dealerCode,
// dealer_name: invoiceData.dealerName,
// amount: invoiceData.amount,
// description: invoiceData.description,
// io_number: invoiceData.ioNumber,
// tax_details: invoiceData.taxDetails
// }, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// eInvoiceNumber: response.data.e_invoice_number,
// dmsNumber: response.data.dms_number,
// invoiceDate: new Date(response.data.invoice_date),
// invoiceUrl: response.data.invoice_url
// };
logger.warn('[DMS] DMS e-invoice generation not implemented, generating mock invoice');
const mockInvoiceNumber = `EINV-${Date.now()}`;
@ -145,31 +121,6 @@ export class DMSIntegrationService {
};
}
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.post(`${this.dmsBaseUrl}/api/credit-notes/generate`, {
// request_number: creditNoteData.requestNumber,
// e_invoice_number: creditNoteData.eInvoiceNumber,
// dealer_code: creditNoteData.dealerCode,
// dealer_name: creditNoteData.dealerName,
// amount: creditNoteData.amount,
// reason: creditNoteData.reason,
// description: creditNoteData.description
// }, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// creditNoteNumber: response.data.credit_note_number,
// creditNoteDate: new Date(response.data.credit_note_date),
// creditNoteAmount: response.data.credit_note_amount,
// creditNoteUrl: response.data.credit_note_url
// };
logger.warn('[DMS] DMS credit note generation not implemented, generating mock credit note');
const mockCreditNoteNumber = `CN-${Date.now()}`;
return {
@ -217,23 +168,7 @@ export class DMSIntegrationService {
};
}
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.get(`${this.dmsBaseUrl}/api/invoices/${eInvoiceNumber}/status`, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: true,
// status: response.data.status,
// invoiceNumber: response.data.invoice_number,
// dmsNumber: response.data.dms_number,
// invoiceDate: new Date(response.data.invoice_date),
// amount: response.data.amount
// };
;
logger.warn('[DMS] DMS invoice status check not implemented, returning mock status');
return {
@ -277,20 +212,7 @@ export class DMSIntegrationService {
};
}
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.get(`${this.dmsBaseUrl}/api/invoices/${eInvoiceNumber}/download`, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`
// },
// responseType: 'arraybuffer'
// });
//
// return {
// success: true,
// documentBuffer: Buffer.from(response.data),
// mimeType: response.headers['content-type'] || 'application/pdf'
// };
logger.warn('[DMS] DMS invoice download not implemented, returning mock URL');
return {

View File

@ -59,10 +59,10 @@ export class SAPIntegrationService {
'sap-client': '200'
});
const fullUrl = `${this.sapBaseUrl}${serviceRootUrl}?${queryParams.toString()}`;
logger.debug(`[SAP] Fetching CSRF token from service: ${serviceName}`);
logger.debug(`[SAP] CSRF token request URL: ${fullUrl}`);
// Use standalone axios request with Basic Auth in header
// We need to capture cookies from this response to use in POST request
const response = await axios.get(fullUrl, {
@ -85,16 +85,16 @@ export class SAPIntegrationService {
});
// SAP returns CSRF token in response headers (check multiple case variations)
const csrfToken = response.headers['x-csrf-token'] ||
response.headers['X-CSRF-Token'] ||
response.headers['X-Csrf-Token'] ||
response.headers['x-csrf-token'];
const csrfToken = response.headers['x-csrf-token'] ||
response.headers['X-CSRF-Token'] ||
response.headers['X-Csrf-Token'] ||
response.headers['x-csrf-token'];
// Extract cookies from response headers
// SAP sets cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext
const setCookieHeaders = response.headers['set-cookie'] as string | string[] | undefined;
let cookies = '';
if (setCookieHeaders) {
if (Array.isArray(setCookieHeaders)) {
// Extract cookie values from Set-Cookie headers
@ -107,36 +107,36 @@ export class SAPIntegrationService {
cookies = setCookieHeaders.split(';')[0].trim();
}
}
// Log full GET response for debugging
logger.info(`[SAP] GET Response Status: ${response.status} ${response.statusText || ''}`);
logger.info(`[SAP] GET Response Headers:`, JSON.stringify(response.headers, null, 2));
logger.info(`[SAP] GET Response Data:`, JSON.stringify(response.data, null, 2));
if (csrfToken && typeof csrfToken === 'string' && csrfToken !== 'fetch') {
logger.info(`[SAP] CSRF token obtained successfully (length: ${csrfToken.length})`);
logger.debug(`[SAP] CSRF token preview: ${csrfToken.substring(0, 20)}...`);
if (cookies) {
logger.debug(`[SAP] Session cookies captured: ${cookies.substring(0, 50)}...`);
} else {
logger.warn('[SAP] No cookies found in CSRF token response - POST may fail');
}
return { csrfToken, cookies };
}
logger.warn('[SAP] CSRF token not found in response headers or invalid');
logger.debug('[SAP] Response status:', response.status);
logger.debug('[SAP] Available headers:', Object.keys(response.headers).filter(h => h.toLowerCase().includes('csrf')));
return null;
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
logger.error(`[SAP] Failed to get CSRF token: ${axiosError.response.status} ${axiosError.response.statusText}`);
logger.error(`[SAP] Response data:`, axiosError.response.data);
if (axiosError.response.status === 401 || axiosError.response.status === 403) {
logger.error('[SAP] Authentication failed while fetching CSRF token - check SAP credentials');
} else if (axiosError.response.status === 404) {
@ -152,7 +152,7 @@ export class SAPIntegrationService {
} else {
logger.error('[SAP] Error setting up CSRF token request:', error instanceof Error ? error.message : 'Unknown error');
}
return null;
}
}
@ -163,7 +163,7 @@ export class SAPIntegrationService {
private createSapClient() {
// Check if SSL verification should be disabled (for testing with self-signed certs)
const disableSSLVerification = process.env.SAP_DISABLE_SSL_VERIFY === 'true';
const client = axios.create({
baseURL: this.sapBaseUrl,
timeout: this.sapTimeout,
@ -265,11 +265,11 @@ export class SAPIntegrationService {
}
const sapClient = this.createSapClient();
// SAP OData endpoint: GetSenderDataSet with filter on IONumber
// Service name is configurable via SAP_SERVICE_NAME env variable
const endpoint = this.buildODataEndpoint('GetSenderDataSet');
// Build OData query parameters matching the working URL format
// $filter: Filter by IO number
// $select: Select specific fields (Sender, ResponseDate, GetIODetailsSet01)
@ -281,18 +281,18 @@ export class SAPIntegrationService {
'$expand': 'GetIODetailsSet01',
'$format': 'json'
});
const fullUrl = `${endpoint}?${queryParams.toString()}`;
logger.info(`[SAP] Validating IO number: ${ioNumber} using service: ${this.sapServiceName}`);
logger.debug(`[SAP] Request URL: ${this.sapBaseUrl}${fullUrl}`);
const response = await sapClient.get(fullUrl);
if (response.status === 200 && response.data) {
// SAP OData response format: { d: { results: [...] } }
const results = response.data.d?.results || response.data.results || [];
if (results.length === 0) {
logger.warn(`[SAP] IO number ${ioNumber} not found in SAP`);
return {
@ -308,11 +308,11 @@ export class SAPIntegrationService {
// Get first result (should be only one for a specific IO number)
const senderData = results[0];
// IO details are in the expanded GetIODetailsSet01 entity set
// Structure: senderData.GetIODetailsSet01.results[0]
const ioDetailsSet = senderData.GetIODetailsSet01;
if (!ioDetailsSet || !ioDetailsSet.results || !Array.isArray(ioDetailsSet.results) || ioDetailsSet.results.length === 0) {
logger.warn(`[SAP] No IO details found in expanded GetIODetailsSet01 for IO ${ioNumber}`);
return {
@ -325,34 +325,34 @@ export class SAPIntegrationService {
error: 'IO details not found in SAP response'
};
}
// Get the first IO detail from the results array
const ioDetails = ioDetailsSet.results[0];
// Map SAP response fields to our format based on actual response structure:
// - AvailableAmount: string with trailing space (e.g., "14333415.00 ")
// - IODescription: description text
// - IONumber: IO number
// - BlockedAmount: may not be present in response, default to 0
// - Currency: may not be present, default to INR
// Parse AvailableAmount - it's a string that may have trailing spaces
const availableAmountStr = (ioDetails.AvailableAmount || '0').toString().trim();
const availableBalance = parseFloat(availableAmountStr) || 0;
// BlockedAmount may not be in the response, default to 0
// If it exists, it might also be a string with trailing space
const blockedAmountStr = (ioDetails.BlockedAmount || ioDetails.Blocked || '0').toString().trim();
const blockedAmount = parseFloat(blockedAmountStr) || 0;
const remainingBalance = availableBalance - blockedAmount;
// Currency may not be in response, default to INR
const currency = (ioDetails.Currency || ioDetails.CurrencyCode || ioDetails.Curr || 'INR').toString().trim();
// Description from IODescription field
const description = ioDetails.IODescription || ioDetails.Description || ioDetails.Text || ioDetails.ShortText || undefined;
// IO Number from the IO details
const validatedIONumber = ioDetails.IONumber || ioDetails.InternalOrder || ioNumber;
@ -416,7 +416,7 @@ export class SAPIntegrationService {
}
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
// SAP returned an error response
logger.error(`[SAP] Error validating IO number ${ioNumber}:`, {
@ -424,7 +424,7 @@ export class SAPIntegrationService {
statusText: axiosError.response.statusText,
data: axiosError.response.data
});
return {
isValid: false,
ioNumber,
@ -494,16 +494,16 @@ export class SAPIntegrationService {
}
const sapClient = this.createSapClient();
// SAP OData endpoint for budget blocking
// Service: ZFI_BUDGET_BLOCK_API_SRV
// Entity Set: RequesterInputSet
const endpoint = `/sap/opu/odata/sap/${this.sapBlockServiceName}/RequesterInputSet`;
// Format current date/time in ISO format: "2025-08-29T10:51:00"
const now = new Date();
const requestDateTime = now.toISOString().replace(/\.\d{3}Z$/, ''); // Remove milliseconds and Z
// Build request payload matching SAP API structure
const requestPayload = {
Request_Date_Time: requestDateTime,
@ -517,26 +517,26 @@ export class SAPIntegrationService {
lt_io_output: [],
ls_response: []
};
logger.info(`[SAP] Blocking budget for IO ${ioNumber}, Amount: ${amount}, Request: ${requestNumber}`);
logger.debug(`[SAP] Budget block request payload:`, JSON.stringify(requestPayload, null, 2));
// Get CSRF token and cookies for POST request (SAP OData requires both)
// SAP sets session cookies during CSRF token fetch that must be included in POST
const csrfData = await this.getCsrfToken(this.sapBlockServiceName);
if (!csrfData || !csrfData.csrfToken) {
logger.warn('[SAP] CSRF token not available, request may fail with CSRF validation error');
logger.warn('[SAP] This is expected if SAP requires CSRF tokens for POST requests');
}
// Build headers with CSRF token, cookies, and other required headers
// Force JSON format via Accept header (SAP returns XML by default for POST)
const headers: Record<string, string> = {
'Accept': 'application/json, application/atom+xml;q=0.9', // Prefer JSON, fallback to XML
'Content-Type': 'application/json'
};
// Add CSRF token if available (required by SAP for POST/PUT/DELETE)
// Use lowercase 'x-csrf-token' as per SAP requirement
if (csrfData?.csrfToken) {
@ -545,7 +545,7 @@ export class SAPIntegrationService {
} else {
logger.warn('[SAP] CSRF token not available - request may fail with CSRF validation error');
}
// Add cookies if available (SAP session cookies required for POST)
// Cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext
if (csrfData?.cookies) {
@ -554,24 +554,24 @@ export class SAPIntegrationService {
} else {
logger.warn('[SAP] No session cookies available - request may fail');
}
// Some SAP systems also require these headers
headers['X-Requested-With'] = 'XMLHttpRequest';
// NOTE: Do NOT add query parameters ($format, sap-client) to POST requests
// SAP OData does not allow SystemQueryOptions in POST requests
// Query parameters are only allowed for GET requests
// Use the endpoint directly without query parameters
const urlWithParams = endpoint;
logger.debug(`[SAP] POST request URL: ${urlWithParams}`);
logger.debug(`[SAP] Request headers (CSRF token and cookies masked):`, {
...headers,
logger.debug(`[SAP] Request headers (CSRF token and cookies masked):`, {
...headers,
'x-csrf-token': csrfData?.csrfToken ? `${csrfData.csrfToken.substring(0, 10)}...` : 'not set',
'Cookie': csrfData?.cookies ? `${csrfData.cookies.substring(0, 30)}...` : 'not set'
});
logger.debug(`[SAP] Using username: ${this.sapUsername}`);
// Ensure auth is explicitly included in POST request config
// The axios instance has auth configured, but we'll include it explicitly to be safe
// This ensures auth is sent even if the instance config is overridden
@ -587,21 +587,21 @@ export class SAPIntegrationService {
}) : undefined,
validateStatus: (status: number) => status < 500 // Don't throw on 4xx
};
logger.debug(`[SAP] POST request config prepared (auth included)`);
const response = await sapClient.post(urlWithParams, requestPayload, postConfig);
// Log full response for debugging
logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`);
logger.info(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2));
// Check if response is XML (SAP returns XML/Atom by default for POST)
const contentType = response.headers['content-type'] || '';
const isXML = contentType.includes('xml') || contentType.includes('atom') ||
(typeof response.data === 'string' && response.data.trim().startsWith('<'));
const isXML = contentType.includes('xml') || contentType.includes('atom') ||
(typeof response.data === 'string' && response.data.trim().startsWith('<'));
let responseData: any = response.data;
// Parse XML if needed
if (isXML && typeof response.data === 'string') {
logger.info(`[SAP] Response is XML, parsing to JSON...`);
@ -624,7 +624,7 @@ export class SAPIntegrationService {
// Continue with original data
}
}
// Log response data summary
if (responseData) {
if (responseData.entry) {
@ -633,11 +633,11 @@ export class SAPIntegrationService {
logger.info(`[SAP] Response has OData 'd' wrapper`);
}
}
// Also log the request that was sent
logger.info(`[SAP] POST Request URL: ${urlWithParams}`);
logger.info(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2));
if (response.status === 200 || response.status === 201) {
// Parse SAP response
// Response structure may vary, but typically contains:
@ -645,7 +645,7 @@ export class SAPIntegrationService {
// - Blocked amount confirmation
// - Remaining balance (in lt_io_output[0].Available_Amount for XML)
// - Block ID or reference number
// Helper function to extract remaining balance from various field names
// For XML: Available_Amount in lt_io_output[0] (may be prefixed with 'd:' namespace)
// For JSON: RemainingBalance, Remaining, Available_Amount, etc.
@ -654,43 +654,43 @@ export class SAPIntegrationService {
logger.debug(`[SAP] extractRemainingBalance: obj is null/undefined`);
return 0;
}
// Helper to extract value from field (handles both direct values and nested #text nodes)
const getFieldValue = (fieldName: string): any => {
const field = obj[fieldName];
if (field === undefined || field === null) return null;
// If it's an object with #text property (XML parser sometimes does this)
if (typeof field === 'object' && field['#text'] !== undefined) {
return field['#text'];
}
// Direct value
return field;
};
// Try various field name variations (both JSON and XML formats)
// XML namespace prefixes: 'd:Available_Amount', 'd:RemainingBalance', etc.
// IMPORTANT: Check 'd:Available_Amount' first as that's what SAP returns in XML
// Also check without namespace prefix as parser might strip it
const value = getFieldValue('d:Available_Amount') ?? // XML format with namespace prefix (PRIORITY)
getFieldValue('Available_Amount') ?? // XML format without prefix (parser might strip 'd:')
getFieldValue('d:AvailableAmount') ?? // CamelCase variation with prefix
getFieldValue('AvailableAmount') ?? // CamelCase variation without prefix
getFieldValue('d:RemainingBalance') ??
getFieldValue('RemainingBalance') ??
getFieldValue('RemainingAmount') ??
getFieldValue('Remaining') ??
getFieldValue('AvailableBalance') ??
getFieldValue('Balance') ??
getFieldValue('Available') ??
null;
getFieldValue('Available_Amount') ?? // XML format without prefix (parser might strip 'd:')
getFieldValue('d:AvailableAmount') ?? // CamelCase variation with prefix
getFieldValue('AvailableAmount') ?? // CamelCase variation without prefix
getFieldValue('d:RemainingBalance') ??
getFieldValue('RemainingBalance') ??
getFieldValue('RemainingAmount') ??
getFieldValue('Remaining') ??
getFieldValue('AvailableBalance') ??
getFieldValue('Balance') ??
getFieldValue('Available') ??
null;
if (value === null || value === undefined) {
logger.debug(`[SAP] extractRemainingBalance: No value found. Object keys:`, Object.keys(obj));
// Log all keys that might be relevant
const relevantKeys = Object.keys(obj).filter(k =>
k.toLowerCase().includes('available') ||
const relevantKeys = Object.keys(obj).filter(k =>
k.toLowerCase().includes('available') ||
k.toLowerCase().includes('amount') ||
k.toLowerCase().includes('remaining') ||
k.toLowerCase().includes('balance')
@ -700,47 +700,47 @@ export class SAPIntegrationService {
}
return 0;
}
// Convert to string first, then parse (handles both string "14291525.00" and number)
const valueStr = value?.toString().trim() || '0';
const parsed = parseFloat(valueStr);
if (isNaN(parsed)) {
logger.warn(`[SAP] extractRemainingBalance: Failed to parse value "${valueStr}" as number`);
return 0;
}
logger.debug(`[SAP] extractRemainingBalance: Extracted value "${valueStr}" -> ${parsed}`);
return parsed;
};
// Helper function to extract blocked amount
const extractBlockedAmount = (obj: any): number => {
if (!obj) return amount;
const value = obj.BlockedAmount ||
obj.Amount ||
obj.Blocked ||
amount.toString();
const value = obj.BlockedAmount ||
obj.Amount ||
obj.Blocked ||
amount.toString();
const parsed = parseFloat(value?.toString() || amount.toString());
return isNaN(parsed) ? amount : parsed;
};
// Handle different possible response structures
let success = false;
let blockedAmount = amount;
let remainingBalance = 0;
let blockId: string | undefined;
// Parse XML structure: entry -> link[@rel='lt_io_output'] -> inline -> feed -> entry -> content -> properties
// Or JSON structure: { d: {...} } or { lt_io_output: [...] }
// Check for XML structure first (parsed XML from fast-xml-parser)
let ioOutputData: any = null;
let message = '';
let mainEntryProperties: any = null;
// XML structure: entry.link (array) -> find link with @rel='lt_io_output' -> inline.feed.entry
// OR JSON OData format: { d: { lt_io_output: { results: [...] } } }
if (responseData.d) {
@ -773,7 +773,7 @@ export class SAPIntegrationService {
responseData = responseData.d;
}
}
// Sometimes XML parser might create the root element with a different name
// Check if responseData itself IS the entry (if root element was <entry>)
let actualEntry = responseData.entry;
@ -783,25 +783,25 @@ export class SAPIntegrationService {
actualEntry = responseData;
}
}
// Check if responseData might be an array (sometimes XML parser returns arrays)
if (Array.isArray(responseData) && responseData.length > 0 && responseData[0]?.entry) {
responseData = responseData[0];
}
// Use actualEntry if we found it, otherwise try responseData.entry
const entry = actualEntry || responseData.entry;
if (entry && !ioOutputData) {
// Also check main entry properties (sometimes Available_Amount is here)
const mainContent = entry.content || {};
mainEntryProperties = mainContent['m:properties'] || mainContent.properties || (mainContent['@_type'] === 'application/xml' ? mainContent : null);
if (mainEntryProperties) {
logger.info(`[SAP] Found main entry properties, keys:`, Object.keys(mainEntryProperties));
}
// Find lt_io_output link in XML structure
const links = Array.isArray(entry.link) ? entry.link : (entry.link ? [entry.link] : []);
const ioOutputLink = links.find((link: any) => {
@ -809,15 +809,15 @@ export class SAPIntegrationService {
const title = link['@_title'] || link.title || '';
return rel.includes('lt_io_output') || title === 'IOOutputSet' || title === 'lt_io_output';
});
if (ioOutputLink?.inline?.feed?.entry) {
const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry)
? ioOutputLink.inline.feed.entry[0]
const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry)
? ioOutputLink.inline.feed.entry[0]
: ioOutputLink.inline.feed.entry;
const content = ioEntry.content || {};
const properties = content['m:properties'] || content.properties || (content['@_type'] === 'application/xml' ? content : null);
if (properties) {
ioOutputData = properties;
message = ioOutputData['d:Message'] || ioOutputData.Message || ioOutputData['#text'] || '';
@ -825,7 +825,7 @@ export class SAPIntegrationService {
}
}
}
// Extract data from ioOutputData (already extracted above for both XML and JSON formats)
if (ioOutputData) {
// XML parsed structure - extract from lt_io_output properties
@ -833,7 +833,7 @@ export class SAPIntegrationService {
success = message.includes('Successful') || message.includes('Success') || !message.includes('Error');
blockedAmount = amount; // Use the amount we sent (from lt_io_input)
remainingBalance = extractRemainingBalance(ioOutputData); // Available_Amount from XML
// If not found in lt_io_output, try main entry properties
if (remainingBalance === 0 && mainEntryProperties) {
logger.info(`[SAP] Available_Amount not found in lt_io_output, trying main entry properties`);
@ -842,51 +842,51 @@ export class SAPIntegrationService {
logger.info(`[SAP] Found Available_Amount in main entry properties: ${remainingBalance}`);
}
}
// Helper function to extract SAP reference number (similar to extractRemainingBalance)
const extractSapReference = (obj: any): string | undefined => {
if (!obj) return undefined;
const getFieldValue = (fieldName: string): any => {
const field = obj[fieldName];
if (field === undefined || field === null) return null;
// If it's an object with #text property (XML parser sometimes does this)
if (typeof field === 'object' && field['#text'] !== undefined) {
return field['#text'];
}
// Direct value
return field;
};
// Try various field name variations for SAP reference number
const value = getFieldValue('d:Sap_Reference_no') ?? // XML format with namespace prefix (PRIORITY)
getFieldValue('Sap_Reference_no') ?? // XML format without prefix
getFieldValue('d:SapReferenceNo') ??
getFieldValue('SapReferenceNo') ??
getFieldValue('d:Reference') ??
getFieldValue('Reference') ??
getFieldValue('d:BlockId') ??
getFieldValue('BlockId') ??
getFieldValue('d:DocumentNumber') ??
getFieldValue('DocumentNumber') ??
null;
getFieldValue('Sap_Reference_no') ?? // XML format without prefix
getFieldValue('d:SapReferenceNo') ??
getFieldValue('SapReferenceNo') ??
getFieldValue('d:Reference') ??
getFieldValue('Reference') ??
getFieldValue('d:BlockId') ??
getFieldValue('BlockId') ??
getFieldValue('d:DocumentNumber') ??
getFieldValue('DocumentNumber') ??
null;
if (value === null || value === undefined) {
logger.debug(`[SAP] extractSapReference: No value found. Object keys:`, Object.keys(obj));
return undefined;
}
// Convert to string and trim
const valueStr = String(value).trim();
logger.debug(`[SAP] extractSapReference: Extracted value "${valueStr}"`);
return valueStr || undefined;
};
// Extract SAP reference number using helper function
blockId = extractSapReference(ioOutputData) || extractSapReference(mainEntryProperties) || undefined;
// Log detailed information for debugging
logger.info(`[SAP] Extracted from XML lt_io_output:`, {
message,
@ -896,10 +896,10 @@ export class SAPIntegrationService {
sampleKeys: Object.keys(ioOutputData).slice(0, 10), // First 10 keys for debugging
foundInMainEntry: remainingBalance > 0 && mainEntryProperties ? true : false,
ioOutputDataSample: Object.keys(ioOutputData).reduce((acc: any, key: string) => {
if (key.toLowerCase().includes('available') ||
key.toLowerCase().includes('amount') ||
key.toLowerCase().includes('reference') ||
key.toLowerCase().includes('sap')) {
if (key.toLowerCase().includes('available') ||
key.toLowerCase().includes('amount') ||
key.toLowerCase().includes('reference') ||
key.toLowerCase().includes('sap')) {
acc[key] = ioOutputData[key];
}
return acc;
@ -933,7 +933,7 @@ export class SAPIntegrationService {
logger.warn('[SAP] Budget block response structure unclear, assuming success');
logger.warn('[SAP] Response data keys:', Object.keys(responseData || {}));
}
// Log what we extracted
logger.info(`[SAP] Extracted from response:`, {
success,
@ -947,7 +947,7 @@ export class SAPIntegrationService {
hasMainEntryProperties: !!mainEntryProperties,
mainEntryPropertiesKeys: mainEntryProperties ? Object.keys(mainEntryProperties) : null
});
// If ioOutputData exists but we didn't extract values, log detailed info
if (ioOutputData && (remainingBalance === 0 || !blockId)) {
logger.warn(`[SAP] ⚠️ ioOutputData exists but extraction failed. Full ioOutputData:`, JSON.stringify(ioOutputData, null, 2));
@ -957,7 +957,7 @@ export class SAPIntegrationService {
return acc;
}, {}));
}
// If remaining balance is 0, log the full response structure for debugging
if (remainingBalance === 0 && response.status === 200 || response.status === 201) {
logger.warn(`[SAP] ⚠️ Remaining balance is 0, but request was successful. Full response structure:`, JSON.stringify(responseData, null, 2));
@ -965,7 +965,7 @@ export class SAPIntegrationService {
if (success) {
logger.info(`[SAP] Budget blocked successfully for IO ${ioNumber}. Blocked: ${blockedAmount}, Remaining: ${remainingBalance}`);
// Only return blockId if SAP provided a reference number
// Don't generate a fallback - we want the actual SAP document number
if (blockId) {
@ -973,7 +973,7 @@ export class SAPIntegrationService {
} else {
logger.warn(`[SAP] ⚠️ No SAP Reference Number (Sap_Reference_no) found in response`);
}
return {
success: true,
blockId: blockId || undefined, // Only return actual SAP reference number, no fallback
@ -994,7 +994,7 @@ export class SAPIntegrationService {
logger.error(`[SAP] Authentication failed during budget blocking (Status: ${response.status}) - check SAP credentials`);
logger.error(`[SAP] Response data:`, response.data);
logger.error(`[SAP] Response headers:`, response.headers);
// Check if it's actually a CSRF error disguised as auth error
const responseText = JSON.stringify(response.data || {});
if (responseText.includes('CSRF') || responseText.includes('csrf') || responseText.includes('token')) {
@ -1006,7 +1006,7 @@ export class SAPIntegrationService {
error: 'SAP CSRF token validation failed - token may have expired or be invalid'
};
}
return {
success: false,
blockedAmount: 0,
@ -1016,15 +1016,15 @@ export class SAPIntegrationService {
} else {
// Handle 400 Bad Request - usually means invalid request format
let errorMessage = `SAP API returned status ${response.status}`;
if (response.status === 400) {
errorMessage = 'SAP API returned 400 Bad Request - check request payload format';
// Try to extract error message from response
if (response.data) {
try {
const errorData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
// Check for common SAP error fields
if (errorData.error) {
errorMessage = errorData.error.message?.value || errorData.error.message || errorMessage;
@ -1035,14 +1035,14 @@ export class SAPIntegrationService {
} else if (errorData.d?.error) {
errorMessage = errorData.d.error.message?.value || errorData.d.error.message || errorMessage;
}
logger.error(`[SAP] SAP Error Details:`, JSON.stringify(errorData, null, 2));
} catch (e) {
logger.error(`[SAP] Error parsing response data:`, response.data);
}
}
}
logger.error(`[SAP] Unexpected response status during budget blocking: ${response.status}`);
logger.error(`[SAP] Response data:`, response.data);
return {
@ -1054,7 +1054,7 @@ export class SAPIntegrationService {
}
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
// SAP returned an error response
logger.error(`[SAP] Error blocking budget for IO ${ioNumber}:`, {
@ -1062,7 +1062,7 @@ export class SAPIntegrationService {
statusText: axiosError.response.statusText,
data: axiosError.response.data
});
return {
success: false,
blockedAmount: 0,
@ -1117,22 +1117,7 @@ export class SAPIntegrationService {
};
}
// TODO: Implement actual SAP API call to release budget
// Example:
// const response = await axios.post(`${this.sapBaseUrl}/api/io/${ioNumber}/release`, {
// block_id: blockId,
// reference: requestNumber
// }, {
// headers: {
// 'Authorization': `Bearer ${this.sapApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// releasedAmount: response.data.released_amount
// };
logger.warn('[SAP] SAP budget release not implemented, simulating release');
return {
@ -1177,23 +1162,7 @@ export class SAPIntegrationService {
};
}
// TODO: Implement actual SAP API call to get dealer info
// Example:
// const response = await axios.get(`${this.sapBaseUrl}/api/dealers/${dealerCode}`, {
// headers: {
// 'Authorization': `Bearer ${this.sapApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// isValid: response.data.valid,
// dealerCode: response.data.dealer_code,
// dealerName: response.data.dealer_name,
// dealerEmail: response.data.dealer_email,
// dealerPhone: response.data.dealer_phone,
// dealerAddress: response.data.dealer_address
// };
logger.warn('[SAP] SAP dealer lookup not implemented, returning mock data');
return {