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