removed suspicious comments
This commit is contained in:
parent
17c62d2b45
commit
9060c39f9c
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user