diff --git a/controller/validate/index.ts b/controller/validate/index.ts index 66c38b3b..fc018fe3 100644 --- a/controller/validate/index.ts +++ b/controller/validate/index.ts @@ -1,13 +1,49 @@ import { Response, Request } from 'express' import _ from 'lodash' -import { validateActionSchema } from '../../shared/validateLogs' import { logger } from '../../shared/logger' import { DOMAIN, IHttpResponse } from '../../shared/types' -import { actionsArray } from '../../constants' import helper from './helper' import { verify, hash } from '../../shared/crypto' +import { dropDB } from '../../shared/dao' +// Retail 1.2.5 +import { checkSearch as checkSearch125 } from '../../utils/Retail_.1.2.5/Search/search' +import { checkOnsearch as checkOnSearch125 } from '../../utils/Retail_.1.2.5/Search/on_search' +import { checkOnsearchFullCatalogRefresh as checkOnSearchRET11 } from '../../utils/Retail_.1.2.5/RET11_onSearch/onSearch' +import { checkSelect as checkSelect125 } from '../../utils/Retail_.1.2.5/Select/select' +import { checkOnSelect as checkOnSelect125 } from '../../utils/Retail_.1.2.5/Select/onSelect' +import { checkInit as checkInit125 } from '../../utils/Retail_.1.2.5/Init/init' +import { checkOnInit as checkOnInit125 } from '../../utils/Retail_.1.2.5/Init/onInit' +import { checkCancel as checkCancel125 } from '../../utils/Retail_.1.2.5/Cancel/cancel' +import { checkOnCancel as checkOnCancel125 } from '../../utils/Retail_.1.2.5/Cancel/onCancel' +import { checkConfirm as checkConfirm125 } from '../../utils/Retail_.1.2.5/Confirm/confirm' +import { checkOnConfirm as checkOnConfirm125 } from '../../utils/Retail_.1.2.5/Confirm/onConfirm' +import { checkOnStatusPending as checkOnStatusPending125 } from '../../utils/Retail_.1.2.5/Status/onStatusPending' +import { checkOnStatusPacked as checkOnStatusPacked125 } from '../../utils/Retail_.1.2.5/Status/onStatusPacked' +import { checkOnStatusPicked as checkOnStatusPicked125 } from '../../utils/Retail_.1.2.5/Status/onStatusPicked' +import { checkOnStatusOutForDelivery as checkOnStatusOutForDelivery125 } from '../../utils/Retail_.1.2.5/Status/onStatusOutForDelivery' +import { checkOnStatusDelivered as checkOnStatusDelivered125 } from '../../utils/Retail_.1.2.5/Status/onStatusDelivered' +import { checkOnStatusAgentAssigned as checkOnStatusAgentAssigned125 } from '../../utils/Retail_.1.2.5/Status/onStatusAgentAssigned' +import { checkOnStatusAtPickup as checkOnStatusAtPickup125 } from '../../utils/Retail_.1.2.5/Status/onStatusAtPickup' +import { checkOnStatusOutForPickup as checkOnStatusOutForPickup125 } from '../../utils/Retail_.1.2.5/Status/onStatusOutForPickup' +import { checkOnStatusPickupFailed as checkOnStatusPickupFailed125 } from '../../utils/Retail_.1.2.5/Status/onStatusPickupFailed' +import { checkOnStatusInTransit as checkOnStatusInTransit125 } from '../../utils/Retail_.1.2.5/Status/onStatusInTransit' +import { checkOnStatusAtDestinationHub as checkOnStatusAtDestinationHub125 } from '../../utils/Retail_.1.2.5/Status/onStatusAtDestinationHub' +import { checkOnStatusAtDelivery as checkOnStatusAtDelivery125 } from '../../utils/Retail_.1.2.5/Status/onStatusAtDelivery' +import { checkOnStatusDeliveryFailed as checkOnStatusDeliveryFailed125 } from '../../utils/Retail_.1.2.5/Status/onStatusDeliveryFailed' +import { checkOnStatusRTODelivered as checkOnStatusRTODelivered125 } from '../../utils/Retail_.1.2.5/Status/onStatusRTODelivered' +import { checkTrack as checkTrack125 } from '../../utils/Retail_.1.2.5/Track/track' +import { checkOnTrack as checkOnTrack125 } from '../../utils/Retail_.1.2.5/Track/onTrack' +import { checkUpdate as checkUpdate125 } from '../../utils/Retail_.1.2.5/Update/update' +import { checkOnUpdate as checkOnUpdate125 } from '../../utils/Retail_.1.2.5/Update/onUpdate' +import { checkSearchIncremental as checkSearchIncremental125 } from '../../utils/Retail_.1.2.5/SearchInc/searchIncremental' +import { checkOnsearchIncremental as checkOnSearchIncremental125 } from '../../utils/Retail_.1.2.5/SearchInc/onSearchIncremental' +import { ApiSequence } from '../../constants' +// Retail 1.2.0 +import { checkSearch as checkSearch120 } from '../../utils/Retail/Search/search' +import { checkOnsearch as checkOnSearch120 } from '../../utils/Retail/Search/on_search' + const controller = { validate: async (req: Request, res: Response): Promise => { console.log("++++++++++++++++ Validate is called") @@ -82,7 +118,7 @@ const controller = { const { signature, currentDate } = await helper.createSignature({ message: JSON.stringify(httpResponse) }) - if(!success && response)return res.status(200).send({ success, response: httpResponse, signature, signTimestamp: currentDate }) + if (!success && response) return res.status(200).send({ success, response: httpResponse, signature, signTimestamp: currentDate }) if (!success) return res.status(400).send({ success, response: httpResponse, signature, signTimestamp: currentDate }) @@ -153,40 +189,225 @@ const controller = { validateSingleAction: async (req: Request, res: Response): Promise => { try { - let error if (!req.body) return res.status(400).send({ success: false, error: 'provide transaction logs to verify' }) - const { context, message } = req.body - if (!context || !message) return res.status(400).send({ success: false, error: 'context, message are required' }) + const { payload, flow, stateless: topLevelStateless, schemaValidation } = req.body + const context = payload?.context + const message = payload?.message + + if (!context || !message) return res.status(400).send({ success: false, error: 'context, message are required' }) if (!context.domain || !context.core_version || !context.action) { return res .status(400) .send({ success: false, error: 'context.domain, context.core_version, context.action is required' }) } - const { domain, core_version, action } = req.body.context - if (!actionsArray.includes(action)) { - return res.status(400).send({ success: false, error: 'context.action should be valid' }) + const { domain, core_version, action } = context + const domainShort = domain.split(':')[1] + logger.info(`validateSingleAction: domain=${domain}, domainShort=${domainShort}, action=${action}, core_version=${core_version}`) + + await dropDB() + const msgIdSet = new Set() + let error: any = {} + + // Reconstruct the full data object like the regular validate endpoint expects + const fullData = { context, message } + + // Helper: pick schema/business/combined errors from validator buckets + const pickErrors = (result: any, flag: boolean | undefined) => { + if (!result || typeof result !== 'object') return {} + if (flag === true) return result.schemaErrors || {} + if (flag === false) return result.businessErrors || {} + if (!result.schemaErrors && !result.businessErrors) { + return result // Return the flat error object directly + } + return { ...(result.schemaErrors || {}), ...(result.businessErrors || {}) } } - const payload = req.body switch (core_version) { - case '1.2.0': case '1.2.5': - error = validateActionSchema(payload, domain, action) + switch (action) { + case 'search': + if (!context.city) { + error = { fulfillments: 'context.city is required for search' } + break + } + if (context.city != "*") { + //search full catalog + logger.info(`validateSingleAction: calling checkSearch125 for domain ${domainShort}`) + error = checkSearch125(fullData, msgIdSet, flow, topLevelStateless ?? true, undefined) + logger.info(`validateSingleAction: checkSearch125 result:`, error) + } else { + //incremental + logger.info(`validateSingleAction: calling checkOnsearchIncremental125 for domain ${domainShort}`) + error = checkSearchIncremental125(fullData, msgIdSet) + logger.info(`validateSingleAction: checkOnsearchIncremental125 result:`, error) + } + + break + case 'on_search': + if (domainShort === 'RET11') { + logger.info(`validateSingleAction: calling checkOnSearchRET11 for domain ${domainShort}`) + error = checkOnSearchRET11(fullData, flow, topLevelStateless ?? true, undefined) + } else { + if (context.city != "*") { + logger.info(`validateSingleAction: calling checkOnSearch125 for domain ${domainShort}`) + error = checkOnSearch125(fullData, flow, topLevelStateless ?? true, undefined) + } else { + logger.info(`validateSingleAction: calling checkOnSearchIncremental125 for domain ${domainShort}`) + error = checkOnSearchIncremental125(fullData, msgIdSet) + } + } + logger.info(`validateSingleAction: on_search result:`, error) + break + case 'select': + logger.info(`validateSingleAction: calling checkSelect125 for domain ${domainShort}`) + error = checkSelect125(fullData, msgIdSet, ApiSequence.SELECT, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkSelect125 result:`, error) + break + case 'on_select': + logger.info(`validateSingleAction: calling checkOnSelect125 for domain ${domainShort}`) + error = checkOnSelect125(fullData, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkOnSelect125 result:`, error) + break + case 'init': + logger.info(`validateSingleAction: calling checkInit125 for init in domain ${domainShort}`) + error = checkInit125(fullData, msgIdSet, ApiSequence.INIT, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkInit125 for init result:`, error) + break + case 'on_init': + logger.info(`validateSingleAction: calling checkOnSelect125 for on_init in domain ${domainShort}`) + error = checkOnInit125(fullData, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkOnSelect125 for on_init result:`, error) + break + case 'cancel': + logger.info(`validateSingleAction: calling checkCancel125 for domain ${domainShort}`) + error = checkCancel125(fullData, msgIdSet, ApiSequence.CANCEL, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkCancel125 result:`, error) + break + case 'on_cancel': + logger.info(`validateSingleAction: on_cancel action is not implemented yet for domain ${domainShort}`) + error = checkOnCancel125(fullData, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkOnCancel result:`, error) + break + case 'confirm': + logger.info(`validateSingleAction: calling checkConfirm125 for domain ${domainShort}`) + error = checkConfirm125(fullData, msgIdSet, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkConfirm result:`, error) + break + case 'on_confirm': + logger.info(`validateSingleAction: calling checkOnConfirm for domain ${domainShort}`) + error = checkOnConfirm125(fullData, flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkOnConfirm result:`, error) + break + case 'on_status': + logger.info(`validateSingleAction: on_status call for domain ${domainShort}`) + if (!message.order.fulfillments || !message.order.fulfillments.length) { + error = { fulfillments: 'message.fulfillments is required for on_status' } + break + } + const fulfillmentState = message.order.fulfillments[0]?.state?.descriptor?.code + logger.info(`validateSingleAction: on_status with fulfillment state: ${fulfillmentState}`) + + switch (fulfillmentState) { + case 'Pending': + error = checkOnStatusPending125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'Packed': //Check Errors + error = checkOnStatusPacked125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'Agent-assigned': + error = checkOnStatusAgentAssigned125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'At-pickup': + error = checkOnStatusAtPickup125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'Out-for-pickup': //Check Errors + error = checkOnStatusOutForPickup125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'Pickup-failed': //Check Errors + error = checkOnStatusPickupFailed125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'Order-picked-up': //Check Errors + error = checkOnStatusPicked125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'In-transit': //Check Errors + error = checkOnStatusInTransit125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'At-destination-hub': //Check Errors + error = checkOnStatusAtDestinationHub125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'At-delivery': //Check Errors + error = checkOnStatusAtDelivery125(fullData, fulfillmentState, msgIdSet, new Set(), flow, schemaValidation, topLevelStateless ?? true) + break + case 'Out-for-delivery': + error = checkOnStatusOutForDelivery125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'Delivery-failed': //Check Errors + error = checkOnStatusDeliveryFailed125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'Order-delivered': + error = checkOnStatusDelivered125(fullData, fulfillmentState, msgIdSet, new Set(), schemaValidation, topLevelStateless ?? true) + break + case 'RTO-Delivered': //Check Errors + error = checkOnStatusRTODelivered125(fullData, schemaValidation, topLevelStateless ?? true) + break + default: + error = { + fulfillmentState: `Unsupported/unimplemented fulfillment state for on_status: ${fulfillmentState}`, + } + } + logger.info(`validateSingleAction: checkOnStatus result:`, error) + break + case 'track': + logger.info(`validateSingleAction: calling checkTrack125 for domain ${domainShort}`) + error = checkTrack125(fullData, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkTrack125 result:`, error) + break + case 'on_track': + logger.info(`validateSingleAction: calling checkOnTrack125 for domain ${domainShort}`) + error = checkOnTrack125(fullData, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkOnTrack125 result:`, error) + break + case 'update': + logger.info(`validateSingleAction: calling checkUpdate125 for domain ${domainShort}`) + error = checkUpdate125(fullData, msgIdSet, ApiSequence.UPDATE, new Set(), flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkUpdate125 result:`, error) + break + case 'on_update': + logger.info(`validateSingleAction: calling checkUpdate125 for domain ${domainShort}`) + error = checkOnUpdate125(fullData, msgIdSet, ApiSequence.ON_UPDATE, new Set(), new Set(), new Set(), flow, schemaValidation, topLevelStateless ?? true) + logger.info(`validateSingleAction: checkUpdate125 result:`, error) + break + default: + return res.status(400).send({ success: false, error: `Unsupported action for retail 1.2.5: ${action}` }) + } + break + case '1.2.0': + switch (action) { + case 'search': + error = checkSearch120({ context, message }, msgIdSet) + break + case 'on_search': + error = checkOnSearch120({ context, message }) + break + default: + return res.status(400).send({ success: false, error: `Unsupported action for retail 1.2.0: ${action}` }) + } break default: - logger.warn('Invalid core_version !! ') - res.status(400).send({ success: false, error: 'Invalid core_version, Please Enter a valid core_version' }) - return + return res.status(400).send({ success: false, error: 'Invalid core_version' }) } - if (!_.isEmpty(error)) res.status(400).send({ success: false, error }) - else return res.status(200).send({ success: true, error }) + // Select errors via helper for clarity + const chosenErrors = pickErrors(error, schemaValidation) + + if (chosenErrors && Object.keys(chosenErrors).length) return res.status(400).send({ success: false, error: chosenErrors }) + return res.status(200).send({ success: true, error: false }) } catch (error) { logger.error(error) - return res.status(500).send({ success: false, error: error }) + return res.status(500).send({ success: false, error }) } }, getValidationFormat: async (req: Request, res: Response): Promise => { @@ -209,14 +430,14 @@ const controller = { return res.status(500).send({ success: false, error: error }) } }, - healthCheck: async(req: Request, res: Response): Promise =>{ + healthCheck: async (req: Request, res: Response): Promise => { try { logger.info(req) - return res.status(200).send({success: true, status:"OK"}) + return res.status(200).send({ success: true, status: "OK" }) } - catch(error){ + catch (error) { logger.error(error) - return res.status(500).send({success: false, status:"fail"}) + return res.status(500).send({ success: false, status: "fail" }) } } } diff --git a/package.json b/package.json index 99fa3de2..5fde90d9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "homepage": "https://github.com/ONDC-Official/log-validation-utility", "dependencies": { + "@anthropic-ai/claude-code": "^1.0.92", "@types/js-yaml": "^4.0.9", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", diff --git a/shared/schemaValidatorV2.ts b/shared/schemaValidatorV2.ts index 7e36adff..0c0560ca 100644 --- a/shared/schemaValidatorV2.ts +++ b/shared/schemaValidatorV2.ts @@ -79,13 +79,13 @@ import { cancelSchemaTRV_12 } from '../schema/TRV-12/cancel' import { onConfirmSchemaTRV_12 } from '../schema/TRV-12/on_confirm' import { onCancelSchemaTRV_12 } from '../schema/TRV-12/on_cancel' import { searchFIS14Schema } from '../schema/FIS/Mutual_Funds/search' -import {onSearchFIS14Schema} from '../schema/FIS/Mutual_Funds/on_search' -import {selectFIS14Schema} from '../schema/FIS/Mutual_Funds/select' -import {onSelectFIS14Schema} from '../schema/FIS/Mutual_Funds/on_select' -import {initFIS14Schema} from '../schema/FIS/Mutual_Funds/init' -import {onInitFIS14Schema} from '../schema/FIS/Mutual_Funds/on_init' -import {confirmFIS14Schema} from '../schema/FIS/Mutual_Funds/confirm' -import {onConfirmFIS14Schema} from '../schema/FIS/Mutual_Funds/on_confirm' +import { onSearchFIS14Schema } from '../schema/FIS/Mutual_Funds/on_search' +import { selectFIS14Schema } from '../schema/FIS/Mutual_Funds/select' +import { onSelectFIS14Schema } from '../schema/FIS/Mutual_Funds/on_select' +import { initFIS14Schema } from '../schema/FIS/Mutual_Funds/init' +import { onInitFIS14Schema } from '../schema/FIS/Mutual_Funds/on_init' +import { confirmFIS14Schema } from '../schema/FIS/Mutual_Funds/confirm' +import { onConfirmFIS14Schema } from '../schema/FIS/Mutual_Funds/on_confirm' // import { onStatusFIS14Schema } from '../schema/FIS/Mutual_Funds/on_status' import { onUpdateFIS14Schema } from '../schema/FIS/Mutual_Funds/on_update' @@ -122,9 +122,8 @@ const formatted_error = (errors: any) => { errors.forEach((error: any) => { if (!['not', 'oneOf', 'anyOf', 'allOf', 'if', 'then', 'else'].includes(error.keyword)) { const error_dict = { - message: `${error.message}${error.params.allowedValues ? ` (${error.params.allowedValues})` : ''}${ - error.params.allowedValue ? ` (${error.params.allowedValue})` : '' - }${error.params.additionalProperty ? ` (${error.params.additionalProperty})` : ''}`, + message: `${error.message}${error.params.allowedValues ? ` (${error.params.allowedValues})` : ''}${error.params.allowedValue ? ` (${error.params.allowedValue})` : '' + }${error.params.additionalProperty ? ` (${error.params.additionalProperty})` : ''}`, details: error.instancePath, } error_list.push(error_dict) @@ -807,11 +806,41 @@ const validate_schema_cancel_RET11_for_json = (data: any) => { const error_list = validate_schema(data, cancelSchema) return formatted_error(error_list) } +const validate_schema_cancel_RET12_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET13_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET14_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET15_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET16_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET17_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} +const validate_schema_cancel_RET18_for_json = (data: any) => { + const error_list = validate_schema(data, cancelSchema) + return formatted_error(error_list) +} const validate_schema_cancel_RET19_for_json = (data: any) => { const error_list = validate_schema(data, cancelSchema) return formatted_error(error_list) } + + // ON_CANCEL const validate_schema_on_cancel_1_trv14_for_json = (data: any) => { const error_list = validate_schema(data, onCancel1SchemaTRV14) @@ -1302,7 +1331,7 @@ const validate_schema_report_rsf_for_json = (data: any) => { console.log('error_list of reporrt', formatted_error(error_list)) return formatted_error(error_list) } - + const validate_schema_on_report_rsf_for_json = (data: any) => { const error_list = validate_schema(data, onReportSchema) return formatted_error(error_list) @@ -1367,6 +1396,13 @@ export default { validate_schema_on_confirm_RET19_for_json, validate_schema_cancel_RET11_for_json, validate_schema_cancel_RET19_for_json, + validate_schema_cancel_RET15_for_json, + validate_schema_cancel_RET16_for_json, + validate_schema_cancel_RET17_for_json, + validate_schema_cancel_RET18_for_json, + validate_schema_cancel_RET13_for_json, + validate_schema_cancel_RET14_for_json, + validate_schema_cancel_RET12_for_json, validate_schema_on_cancel_RET11_for_json, // validate_schema_on_cancel_RET19_for_json, validate_schema_track_RET11_for_json, diff --git a/utils/Retail_.1.2.5/Cancel/cancel.ts b/utils/Retail_.1.2.5/Cancel/cancel.ts index 723d56ec..a42d976f 100644 --- a/utils/Retail_.1.2.5/Cancel/cancel.ts +++ b/utils/Retail_.1.2.5/Cancel/cancel.ts @@ -5,10 +5,11 @@ import { logger } from '../../../shared/logger' import { validateSchemaRetailV2, isObjectEmpty, checkContext, checkBppIdOrBapId } from '../..' import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' -import {isValidISO8601Duration} from '../../index' +import { isValidISO8601Duration } from '../../index' -export const checkCancel = (data: any, msgIdSet: any,action:string,flow?:string) => { +export const checkCancel = (data: any, msgIdSet: any, action: string, flow?: string, schemaValidation?: boolean, stateless?: boolean) => { const cnclObj: any = {} + const schemaErrors: any = {} // const forceCnclObj:any = {} try { if (!data || isObjectEmpty(data)) { @@ -21,7 +22,11 @@ export const checkCancel = (data: any, msgIdSet: any,action:string,flow?:string) } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2('RET11', constants.CANCEL, data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2('RET11', constants.CANCEL, data) + : 'skip' + const select: any = getValue(`${ApiSequence.SELECT}`) const contextRes: any = checkContext(context, constants.CANCEL) @@ -30,14 +35,29 @@ export const checkCancel = (data: any, msgIdSet: any,action:string,flow?:string) if (checkBap) Object.assign(cnclObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(cnclObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(cnclObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(cnclObj, contextRes.ERRORS) } + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(cnclObj).length > 0 + + if (!hasSchema && !hasBusiness) return false + + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: cnclObj } + } + + const combinedErrors = { ...schemaErrors, ...cnclObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + + try { logger.info(`Adding Message Id /${constants.CANCEL}`) if (msgIdSet.has(context.message_id)) { @@ -132,8 +152,8 @@ export const checkCancel = (data: any, msgIdSet: any,action:string,flow?:string) } try { if (flow === FLOW.FLOW005) { - console.log("cancel.tags",JSON.stringify(cancel.tags)); - + console.log("cancel.tags", JSON.stringify(cancel.tags)); + if (cancel.cancellation_reason_id !== '052') { cnclObj['invalidCancellationReasonId'] = `In /${constants.FORCE_CANCEL}, cancellation_reason_id must be '052'` @@ -180,7 +200,18 @@ export const checkCancel = (data: any, msgIdSet: any,action:string,flow?:string) logger.error(`!!Some error occurred while checking /${constants.FORCE_CANCEL} API`, error) } - return cnclObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(cnclObj).length > 0 + + if (!hasSchema && !hasBusiness) return false + + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: cnclObj } + } + + const combinedErrors = { ...schemaErrors, ...cnclObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.CANCEL} API`, err) } diff --git a/utils/Retail_.1.2.5/Cancel/onCancel.ts b/utils/Retail_.1.2.5/Cancel/onCancel.ts index 2338e886..8110c953 100644 --- a/utils/Retail_.1.2.5/Cancel/onCancel.ts +++ b/utils/Retail_.1.2.5/Cancel/onCancel.ts @@ -21,8 +21,9 @@ import { import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' -export const checkOnCancel = (data: any, msgIdSet: any) => { +export const checkOnCancel = (data: any, msgIdSet: any, schemaValidation?: boolean, stateless?: boolean) => { const onCnclObj: any = {} + const schemaErrors: any = {} const onConfirmQuote = getValue(`${constants.ON_CONFIRM}/quote`) try { if (!data || isObjectEmpty(data)) { @@ -35,12 +36,12 @@ export const checkOnCancel = (data: any, msgIdSet: any) => { } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) const flow = getValue('flow') - let schemaValidation: any - if (flow === '5') { - schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL_RTO, data) - } else { - schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL, data) - } + const schemaValidationResult = + schemaValidation !== false + ? (flow === '5' + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL_RTO, data) + : validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL, data)) + : 'skip' const select: any = getValue(`${ApiSequence.SELECT}`) const contextRes: any = checkContext(context, constants.ON_CANCEL) const checkBap = checkBppIdOrBapId(context.bap_id) @@ -50,12 +51,25 @@ export const checkOnCancel = (data: any, msgIdSet: any) => { if (checkBap) Object.assign(onCnclObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(onCnclObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(onCnclObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onCnclObj, contextRes.ERRORS) } + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onCnclObj).length > 0 + + if (!hasSchema && !hasBusiness) return false + + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onCnclObj } + } + + const combinedErrors = { ...schemaErrors, ...onCnclObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } if (flow === '4') { try { @@ -346,7 +360,7 @@ export const checkOnCancel = (data: any, msgIdSet: any) => { try { logger.info(`Checking for Item IDs in quote object in /${constants.ON_CANCEL}`) - let cancelFulfillment:any = [] + let cancelFulfillment: any = [] if (flow === '5') { cancelFulfillment = _.filter(on_cancel.fulfillments, { type: 'RTO' })?.[0] } else { @@ -1071,7 +1085,17 @@ export const checkOnCancel = (data: any, msgIdSet: any) => { // logger.error(`!!Some error occurred while checking /${constants.ON_CANCEL} API`, error) // } - return onCnclObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onCnclObj).length > 0 + + if (!hasSchema && !hasBusiness) return false + + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onCnclObj } + } + + const combinedErrors = { ...schemaErrors, ...onCnclObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_CANCEL} API`, err) } diff --git a/utils/Retail_.1.2.5/Confirm/confirm.ts b/utils/Retail_.1.2.5/Confirm/confirm.ts index 2412a66f..e67af95a 100644 --- a/utils/Retail_.1.2.5/Confirm/confirm.ts +++ b/utils/Retail_.1.2.5/Confirm/confirm.ts @@ -19,8 +19,9 @@ import { import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' -export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { +export const checkConfirm = (data: any, msgIdSet: any, flow: string, schemaValidation?: boolean, stateless?: boolean) => { const cnfrmObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.CONFIRM]: 'JSON cannot be empty' } @@ -31,10 +32,13 @@ export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { return { missingFields: '/context, /message, /order or /message/order is missing or empty' } } - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const parentItemIdSet: any = getValue(`parentItemIdSet`) - const select_customIdArray: any = getValue(`select_customIdArray`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.CONFIRM, data) + const searchContext: any = stateless ? undefined : getValue(`${ApiSequence.SEARCH}_context`) + const parentItemIdSet: any = stateless ? undefined : getValue(`parentItemIdSet`) + const select_customIdArray: any = stateless ? undefined : getValue(`select_customIdArray`) + + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.CONFIRM, data) + : 'skip' const contextRes: any = checkContext(context, constants.CONFIRM) @@ -43,11 +47,15 @@ export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { if (checkBap) Object.assign(cnfrmObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(cnfrmObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(cnfrmObj, schemaValidation) + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - cnfrmObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + cnfrmObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } try { @@ -56,7 +64,7 @@ export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { cnfrmObj[`${ApiSequence.CONFIRM}_msgId`] = `Message id should not be same with previous calls` } msgIdSet.add(context.message_id) - setValue(`${ApiSequence.CONFIRM}_msgId`, data.context.message_id) + if (!stateless) setValue(`${ApiSequence.CONFIRM}_msgId`, data.context.message_id) } catch (error: any) { logger.error(`!!Error while checking message id for /${constants.CONFIRM}, ${error.stack}`) } @@ -65,6 +73,19 @@ export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { Object.assign(cnfrmObj, contextRes.ERRORS) } + // In stateless mode, stop after schema/context/basic validations + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(cnfrmObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: cnfrmObj } + } + // Merge schema and business errors into one object + const combinedErrors = { ...schemaErrors, ...cnfrmObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + setValue(`${ApiSequence.CONFIRM}`, data) try { @@ -184,37 +205,37 @@ export const checkConfirm = (data: any, msgIdSet: any, flow :string) => { } logger.info(`Checking vehicle registration for fulfillments in /${constants.CONFIRM}`); -const fulfillments = confirm.fulfillments; + const fulfillments = confirm.fulfillments; -//Vehicle registeration for (Self-Pickup) Kerbside -if (Array.isArray(fulfillments)) { - fulfillments.forEach((fulfillment, index) => { + //Vehicle registeration for (Self-Pickup) Kerbside + if (Array.isArray(fulfillments)) { + fulfillments.forEach((fulfillment, index) => { const type = fulfillment.type; const category = fulfillment['@ondc/org/category']; const vehicle = fulfillment.vehicle; const SELF_PICKUP = 'Self-Pickup' const KERBSIDE = 'Kerbside' - if (flow === FLOW.FLOW002) { - if (fulfillment.type !== "Self-Pickup") { - logger.info(`Fulfillment Type must be present `) - cnfrmObj['ff'] = `Fulfillment Type Self-Pickup must be present for flow : ${flow}` + if (flow === FLOW.FLOW002) { + if (fulfillment.type !== "Self-Pickup") { + logger.info(`Fulfillment Type must be present `) + cnfrmObj['ff'] = `Fulfillment Type Self-Pickup must be present for flow : ${flow}` + } } - } if (type === SELF_PICKUP && category === KERBSIDE) { - if (!vehicle) { - cnfrmObj[`fulfillment${index}_vehicle`] = - `Vehicle is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}`; - } else if (!vehicle.registration) { - cnfrmObj[`fulfillment${index}_vehicle_registration`] = - `Vehicle registration is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}`; - } - } else if (vehicle) { + if (!vehicle) { cnfrmObj[`fulfillment${index}_vehicle`] = - `Vehicle should not be present in fulfillment ${index} with type ${type} and category ${category} in /${constants.CONFIRM}`; + `Vehicle is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}`; + } else if (!vehicle.registration) { + cnfrmObj[`fulfillment${index}_vehicle_registration`] = + `Vehicle registration is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}`; + } + } else if (vehicle) { + cnfrmObj[`fulfillment${index}_vehicle`] = + `Vehicle should not be present in fulfillment ${index} with type ${type} and category ${category} in /${constants.CONFIRM}`; } - }); -} + }); + } try { logger.info(`Checking for number of digits in tax number in message.order.tags[0].list`) if (message.order.tags && isArray(message.order.tags)) { @@ -337,7 +358,7 @@ if (Array.isArray(fulfillments)) { logger.info(`Comparing Quote object for /${constants.ON_SELECT} and /${constants.CONFIRM}`) const on_select_quote: any = getValue('quoteObj') const quoteErrors = compareQuoteObjects(on_select_quote, confirm.quote, constants.ON_SELECT, constants.CONFIRM) - const hasItemWithQuantity = _.some(confirm.quote.breakup, (item:any) => _.has(item, 'item.quantity')) + const hasItemWithQuantity = _.some(confirm.quote.breakup, (item: any) => _.has(item, 'item.quantity')) if (hasItemWithQuantity) { const key = `quantErr` cnfrmObj[key] = @@ -391,14 +412,13 @@ if (Array.isArray(fulfillments)) { logger.error(`!!Error while storing order created and updated timestamps in /${constants.CONFIRM}`) } - try { logger.info(`Comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}`) const oninitQuotePrice: any = getValue('initQuotePrice') const confirmQuotePrice = parseFloat(confirm.quote.price.value) logger.info(`Comparing quote prices of /${constants.ON_INIT} and /${constants.CONFIRM}`) - if (oninitQuotePrice != confirmQuotePrice ) { + if (oninitQuotePrice != confirmQuotePrice) { logger.info( `order quote price in /${constants.CONFIRM} is not equal to the quoted price in /${constants.ON_INIT}`, ) @@ -459,11 +479,11 @@ if (Array.isArray(fulfillments)) { if (cnfrmObj['message/order/transaction_id']) { cnfrmObj['message/order/transaction_id'] = 'Unexpected txn_id found in message/order/confirm' } else { - if (flow === FLOW.FLOW012 ) { + if (flow === FLOW.FLOW012) { logger.info('Skipping transaction_id check for 012 flow') // Skip the transaction_id check for 012 flow } else { - const status = payment_status(payment,flow) + const status = payment_status(payment, flow) if (!status) { cnfrmObj['message/order/transaction_id'] = 'Transaction_id missing in message/order/payment' } @@ -473,27 +493,27 @@ if (Array.isArray(fulfillments)) { logger.error('Error while checking transaction in message/order/payment: ' + err.message) } try { - if (flow === FLOW.FLOW012){ - logger.info('Payment status check in confirm call') - const payment = confirm.payment - if (payment.status !== PAYMENT_STATUS.NOT_PAID) { - logger.error(`Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`); - cnfrmObj.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` - } - } + if (flow === FLOW.FLOW012) { + logger.info('Payment status check in confirm call') + const payment = confirm.payment + if (payment.status !== PAYMENT_STATUS.NOT_PAID) { + logger.error(`Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`); + cnfrmObj.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` + } + } } catch (err: any) { logger.error('Error while checking payment in message/order/payment: ' + err.message); } - + //Payment details for 012 Flow try { if (flow === FLOW.FLOW012) { logger.info(`checking payment object in /${constants.CONFIRM}`) - setValue('confirmPaymentStatus',confirm.payment.status) + setValue('confirmPaymentStatus', confirm.payment.status) if (confirm.payment['@ondc/org/settlement_details'][0]['settlement_counterparty'] != 'buyer-app') { cnfrmObj.sttlmntcntrparty = `settlement_counterparty is expected to be 'buyer-app' in @ondc/org/settlement_details` } - + logger.info(`checking payment details in /${constants.CONFIRM}`) const data = confirm.payment['@ondc/org/settlement_details'][0] if ( @@ -561,8 +581,17 @@ if (Array.isArray(fulfillments)) { logger.error(`!!Error while storing payment settlement details in /${constants.CONFIRM}`) } - return cnfrmObj - } catch (err: any) { + // Return in new format for validateSingleAction compatibility + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(cnfrmObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: cnfrmObj } + } + const combinedErrors = { ...schemaErrors, ...cnfrmObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.CONFIRM} API`, err) + return cnfrmObj // Return original format for backward compatibility } } diff --git a/utils/Retail_.1.2.5/Confirm/onConfirm.ts b/utils/Retail_.1.2.5/Confirm/onConfirm.ts index f7561695..522428e5 100644 --- a/utils/Retail_.1.2.5/Confirm/onConfirm.ts +++ b/utils/Retail_.1.2.5/Confirm/onConfirm.ts @@ -25,899 +25,931 @@ import { import { FLOW, OFFERSFLOW } from '../../enum' import { getValue, setValue } from '../../../shared/dao' import { extractRoutingType, isValidRoutingType } from '../common/routingValidator' -export const checkOnConfirm = (data: any, flow: string) => { - const onCnfrmObj: any = {} - try { - if (!data || isObjectEmpty(data)) { - return { [ApiSequence.ON_CONFIRM]: 'JSON cannot be empty' } - } - const { message, context }: any = data - if (!message || !context || !message.order || isObjectEmpty(message) || isObjectEmpty(message.order)) { - return { missingFields: '/context, /message, /order or /message/order is missing or empty' } - } +export const checkOnConfirm = (data: any, flow: string, schemaValidation?: boolean, stateless?: boolean) => { + // const onCnfrmObj: any = {} + const schemaErrors: any = {} + const businessErrors: any = {} + + if (!data || isObjectEmpty(data)) { + businessErrors[ApiSequence.ON_CONFIRM] = 'JSON cannot be empty' + return { businessErrors } + } + + const { message, context }: any = data + if (!message || !context || !message.order || isObjectEmpty(message) || isObjectEmpty(message.order)) { + businessErrors.missingFields = '/context, /message, /order or /message/order is missing or empty' + return { businessErrors } + } + + // Schema validation + const schemaValidationResult = + schemaValidation !== false ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CONFIRM, data) : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + // Basic context and ID validations + const contextRes: any = checkContext(context, constants.ON_CONFIRM) + const checkBap = checkBppIdOrBapId(context.bap_id) + const checkBpp = checkBppIdOrBapId(context.bpp_id) + + if (checkBap) businessErrors.bap_id = 'context/bap_id should not be a url' + if (checkBpp) businessErrors.bpp_id = 'context/bpp_id should not be a url' + + if (!contextRes?.valid) { + Object.assign(businessErrors, contextRes.ERRORS) + } + + // Message ID and domain comparison (only if not stateless) + if (!stateless) { try { logger.info(`Comparing Message Ids of /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) if (!_.isEqual(getValue(`${ApiSequence.CONFIRM}_msgId`), context.message_id)) { - onCnfrmObj[`${ApiSequence.ON_CONFIRM}_msgId`] = + businessErrors[`${ApiSequence.ON_CONFIRM}_msgId`] = `Message Ids for /${constants.CONFIRM} and /${constants.ON_CONFIRM} api should be same` } } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_SEARCHINC}, ${error.stack}`) + logger.error(`!!Error while checking message id for /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.message_id = `Error while checking message id for /${constants.ON_CONFIRM}: ${error.message}` } - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const parentItemIdSet: any = getValue(`parentItemIdSet`) - const select_customIdArray: any = getValue(`select_customIdArray`) - - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CONFIRM, data) + // Domain comparison + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + businessErrors[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } - const contextRes: any = checkContext(context, constants.ON_CONFIRM) + // If stateless is true, stop after basic validations and return + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors } + } + // Merge schema and business errors into one object + const combinedErrors = { ...schemaErrors, ...businessErrors } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } - const checkBap = checkBppIdOrBapId(context.bap_id) - const checkBpp = checkBppIdOrBapId(context.bpp_id) + // Continue with business logic (only if not stateless) + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) + const parentItemIdSet: any = getValue(`parentItemIdSet`) + const select_customIdArray: any = getValue(`select_customIdArray`) - if (checkBap) Object.assign(onCnfrmObj, { bap_id: 'context/bap_id should not be a url' }) - if (checkBpp) Object.assign(onCnfrmObj, { bpp_id: 'context/bpp_id should not be a url' }) + setValue(`${ApiSequence.ON_CONFIRM}`, data) - if (schemaValidation !== 'error') { - Object.assign(onCnfrmObj, schemaValidation) + try { + logger.info(`Comparing city of /${constants.SEARCH} and /${constants.ON_CONFIRM}`) + if (!_.isEqual(searchContext.city, context.city)) { + businessErrors.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_CONFIRM}` } + } catch (error: any) { + logger.info(`Error while comparing city in /${constants.SEARCH} and /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.city = `Error while comparing city in /${constants.SEARCH} and /${constants.ON_CONFIRM}: ${error.message}` + } - if (!contextRes?.valid) { - Object.assign(onCnfrmObj, contextRes.ERRORS) - } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onCnfrmObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } + try { + logger.info(`Comparing timestamp of /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) + const tmpstmp = getValue('tmpstmp') + if (_.gte(tmpstmp, context.timestamp)) { + businessErrors.tmpstmp = `Timestamp for /${constants.CONFIRM} api cannot be greater than or equal to /${constants.ON_CONFIRM} api` + } else { + const timeDiff = timeDifference(context.timestamp, tmpstmp) + logger.info(timeDiff) + if (timeDiff > 5000) { + businessErrors.tmpstmp = `context/timestamp difference between /${constants.ON_CONFIRM} and /${constants.CONFIRM} should be less than 5 sec` + } + } + setValue('tmpstmp', context.timestamp) + setValue('onCnfrmtmpstmp', context.timestamp) + } catch (error: any) { + logger.info( + `Error while comparing timestamp for /${constants.CONFIRM} and /${constants.ON_CONFIRM} api, ${error.stack}`, + ) + businessErrors.timestamp = `Error while comparing timestamp for /${constants.CONFIRM} and /${constants.ON_CONFIRM}: ${error.message}` + } - setValue(`${ApiSequence.ON_CONFIRM}`, data) + try { + logger.info(`Comparing transaction Ids of /${constants.SELECT} and /${constants.ON_CONFIRM}`) + if (!_.isEqual(getValue('txnId'), context.transaction_id)) { + businessErrors.txnId = `Transaction Id should be same from /${constants.SELECT} onwards` + } + } catch (error: any) { + logger.error( + `!!Error while comparing transaction ids for /${constants.SELECT} and /${constants.ON_CONFIRM} api, ${error.stack}`, + ) + businessErrors.transaction_id = `Error while comparing transaction ids for /${constants.SELECT} and /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing city of /${constants.SEARCH} and /${constants.ON_CONFIRM}`) - if (!_.isEqual(searchContext.city, context.city)) { - onCnfrmObj.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_CONFIRM}` - } - } catch (error: any) { - logger.info(`Error while comparing city in /${constants.SEARCH} and /${constants.ON_CONFIRM}, ${error.stack}`) - } + const on_confirm = message.order - try { - logger.info(`Comparing timestamp of /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) - const tmpstmp = getValue('tmpstmp') - if (_.gte(tmpstmp, context.timestamp)) { - onCnfrmObj.tmpstmp = `Timestamp for /${constants.CONFIRM} api cannot be greater than or equal to /${constants.ON_CONFIRM} api` - } else { - const timeDiff = timeDifference(context.timestamp, tmpstmp) - logger.info(timeDiff) - if (timeDiff > 5000) { - onCnfrmObj.tmpstmp = `context/timestamp difference between /${constants.ON_CONFIRM} and /${constants.CONFIRM} should be less than 5 sec` + try { + logger.info(`Checking Cancellation terms for /${constants.ON_CONFIRM}`) + const OnInitCancellationTerms = getValue('OnInitCancellationTerms') + + // Only compare if cancellation_terms were provided in on_init + if (OnInitCancellationTerms !== undefined) { + const Errors = compareObjects(OnInitCancellationTerms, message.order.cancellation_terms) + if (Errors) { + let i = 0 + const len = Errors.length + while (i < len) { + const key = `cancellationTermsErr${i}` + console.log('Errors[i]', Errors[i]) + businessErrors[key] = `${Errors[i]} when compared with on_init Cancellation terms` + i++ } } - setValue('tmpstmp', context.timestamp) - setValue('onCnfrmtmpstmp', context.timestamp) - } catch (error: any) { - logger.info( - `Error while comparing timestamp for /${constants.CONFIRM} and /${constants.ON_CONFIRM} api, ${error.stack}`, - ) } + } catch (error: any) { + logger.error(`!!Error while checking Cancellation terms for /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.cancellation_terms = `Error while checking Cancellation terms for /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing transaction Ids of /${constants.SELECT} and /${constants.ON_CONFIRM}`) - if (!_.isEqual(getValue('txnId'), context.transaction_id)) { - onCnfrmObj.txnId = `Transaction Id should be same from /${constants.SELECT} onwards` + try { + logger.info(`Checking fulfillment ids for /${constants.ON_CONFIRM}`) + message.order.fulfillments.forEach((fulfillment: any) => { + if (!fulfillment['@ondc/org/TAT']) { + businessErrors[`message.order.fulfillments[${fulfillment.id}]`] = + `'TAT' must be provided in message/order/fulfillments` } - } catch (error: any) { - logger.error( - `!!Error while comparing transaction ids for /${constants.SELECT} and /${constants.ON_CONFIRM} api, ${error.stack}`, - ) - } - - const on_confirm = message.order + const on_select_fulfillment_tat_obj: any = getValue('fulfillment_tat_obj') + const fulfillment_id = fulfillment.id - try { - logger.info(`Checking Cancellation terms for /${constants.ON_CONFIRM}`) - const OnInitCancellationTerms = getValue('OnInitCancellationTerms') - - // Only compare if cancellation_terms were provided in on_init - if (OnInitCancellationTerms !== undefined) { - const Errors = compareObjects(OnInitCancellationTerms, message.order.cancellation_terms) - if (Errors) { - let i = 0 - const len = Errors.length - while (i < len) { - const key = `cancellationTermsErr${i}` - console.log('Errors[i]', Errors[i]) - onCnfrmObj[key] = `${Errors[i]} when compared with on_init Cancellation terms` - i++ - } - } + logger.info(`Checking TAT Mistatch between /${constants.ON_CONFIRM} & /${constants.ON_SELECT}`) + if ( + on_select_fulfillment_tat_obj !== null && + on_select_fulfillment_tat_obj[fulfillment_id] !== isoDurToSec(fulfillment['@ondc/org/TAT']) + ) { + businessErrors[`TAT_Mismatch`] = + `TAT Mistatch between /${constants.ON_CONFIRM} i.e ${isoDurToSec( + fulfillment['@ondc/org/TAT'], + )} seconds & /${constants.ON_SELECT} i.e ${on_select_fulfillment_tat_obj[fulfillment_id]} seconds` } - } catch (error: any) { - logger.error(`!!Error while checking Cancellation terms for /${constants.ON_CONFIRM}, ${error.stack}`) - } - try { - logger.info(`Checking fulfillment ids for /${constants.ON_CONFIRM}`) - message.order.fulfillments.forEach((fulfillment: any) => { - if (!fulfillment['@ondc/org/TAT']) { - onCnfrmObj[`message.order.fulfillments[${fulfillment.id}]`] = - `'TAT' must be provided in message/order/fulfillments` - } - const on_select_fulfillment_tat_obj: any = getValue('fulfillment_tat_obj') - const fulfillment_id = fulfillment.id + }) + } catch (error: any) { + logger.error(`!!Error while Checking fulfillment ids for /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.fulfillments = `Error while Checking fulfillment ids for /${constants.ON_CONFIRM}: ${error.message}` + } - logger.info(`Checking TAT Mistatch between /${constants.ON_CONFIRM} & /${constants.ON_SELECT}`) - if ( - on_select_fulfillment_tat_obj !== null && - on_select_fulfillment_tat_obj[fulfillment_id] !== isoDurToSec(fulfillment['@ondc/org/TAT']) - ) { - onCnfrmObj[`TAT_Mismatch`] = - `TAT Mistatch between /${constants.ON_CONFIRM} i.e ${isoDurToSec(fulfillment['@ondc/org/TAT'])} seconds & /${constants.ON_SELECT} i.e ${on_select_fulfillment_tat_obj[fulfillment_id]} seconds` - } - }) - } catch (error: any) { - logger.error(`!!Error while Checking fulfillment ids for /${constants.ON_CONFIRM}, ${error.stack}`) + try { + logger.info(`Comparing order ids in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) + if (getValue('cnfrmOrdrId') != on_confirm.id) { + businessErrors.orderID = `Order Id mismatches in /${constants.CONFIRM} and /${constants.ON_CONFIRM}` } + } catch (error: any) { + logger.error(`!!Error while trying to fetch order ids in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.order_id = `Error while trying to fetch order ids in /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing order ids in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) - if (getValue('cnfrmOrdrId') != on_confirm.id) { - onCnfrmObj.orderID = `Order Id mismatches in /${constants.CONFIRM} and /${constants.ON_CONFIRM}` + try { + logger.info(`checking created_at and updated_at timestamp in /${constants.ON_CONFIRM}`) + const cnfrmOrdrCrtd = getValue('ordrCrtd') + const cnfrmOrdrUpdtd = getValue('ordrUpdtd') + if (!_.isEmpty(on_confirm?.state)) setValue('orderState', on_confirm.state) + setValue('onCnfrmState', on_confirm.state) + if (on_confirm.state === 'Created' || on_confirm.state === 'Accepted') { + if (cnfrmOrdrCrtd && (!on_confirm.created_at || on_confirm.created_at != cnfrmOrdrCrtd)) { + businessErrors.crtdtmstmp = `order.created_at timestamp mismatches in /${constants.CONFIRM} and /${constants.ON_CONFIRM}` + } + if (on_confirm.updated_at) { + setValue('PreviousUpdatedTimestamp', on_confirm.updated_at) } - } catch (error: any) { - logger.error(`!!Error while trying to fetch order ids in /${constants.ON_CONFIRM}, ${error.stack}`) - } - - try { - logger.info(`checking created_at and updated_at timestamp in /${constants.ON_CONFIRM}`) - const cnfrmOrdrCrtd = getValue('ordrCrtd') - const cnfrmOrdrUpdtd = getValue('ordrUpdtd') - if (!_.isEmpty(on_confirm?.state)) setValue('orderState', on_confirm.state) - setValue('onCnfrmState', on_confirm.state) - if (on_confirm.state === 'Created' || on_confirm.state === 'Accepted') { - if (cnfrmOrdrCrtd && (!on_confirm.created_at || on_confirm.created_at != cnfrmOrdrCrtd)) { - onCnfrmObj.crtdtmstmp = `order.created_at timestamp mismatches in /${constants.CONFIRM} and /${constants.ON_CONFIRM}` - } - - if (on_confirm.updated_at) { - setValue('PreviousUpdatedTimestamp', on_confirm.updated_at) - } - if ( - cnfrmOrdrUpdtd && - (!on_confirm.updated_at || - _.gte(cnfrmOrdrUpdtd, on_confirm.updated_at) || - on_confirm.updated_at != getValue('tmpstmp')) - ) { - onCnfrmObj.updtdtmstmp = `order.updated_at timestamp should be updated as per the context.timestamp (since default fulfillment state is added)` - } + if ( + cnfrmOrdrUpdtd && + (!on_confirm.updated_at || + _.gte(cnfrmOrdrUpdtd, on_confirm.updated_at) || + on_confirm.updated_at != getValue('tmpstmp')) + ) { + businessErrors.updtdtmstmp = `order.updated_at timestamp should be updated as per the context.timestamp (since default fulfillment state is added)` } - } catch (error: any) { - logger.error(`!!Error while checking order timestamps in /${constants.ON_CONFIRM}, ${error.stack}`) } + } catch (error: any) { + logger.error(`!!Error while checking order timestamps in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.order_timestamps = `Error while checking order timestamps in /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Checking provider id and location in /${constants.ON_CONFIRM}`) - if (on_confirm.provider.id != getValue('providerId')) { - onCnfrmObj.prvdrId = `Provider Id mismatches in /${constants.ON_SEARCH} and /${constants.ON_CONFIRM}` - } + try { + logger.info(`Checking provider id and location in /${constants.ON_CONFIRM}`) + if (on_confirm.provider.id != getValue('providerId')) { + businessErrors.prvdrId = `Provider Id mismatches in /${constants.ON_SEARCH} and /${constants.ON_CONFIRM}` + } - if (on_confirm.provider.locations[0].id != getValue('providerLoc')) { - onCnfrmObj.prvdrLoc = `provider.locations[0].id mismatches in /${constants.ON_SEARCH} and /${constants.ON_CONFIRM}` - } - } catch (error: any) { - logger.error(`!!Error while checking provider id and location in /${constants.ON_CONFIRM}, ${error.stack}`) + if (on_confirm.provider.locations[0].id != getValue('providerLoc')) { + businessErrors.prvdrLoc = `provider.locations[0].id mismatches in /${constants.ON_SEARCH} and /${constants.ON_CONFIRM}` } + } catch (error: any) { + logger.error(`!!Error while checking provider id and location in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.provider = `Error while checking provider id and location in /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing item Ids and fulfillment ids in /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) - const itemFlfllmnts: any = getValue('itemFlfllmnts') - const itemsIdList: any = getValue('itemsIdList') - let i = 0 - const len = on_confirm.items.length + try { + logger.info(`Comparing item Ids and fulfillment ids in /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) + const itemFlfllmnts: any = getValue('itemFlfllmnts') + const itemsIdList: any = getValue('itemsIdList') + let i = 0 + const len = on_confirm.items.length - while (i < len) { - const itemId = on_confirm.items[i].id - const item = on_confirm.items[i] + while (i < len) { + const itemId = on_confirm.items[i].id + const item = on_confirm.items[i] - if (checkItemTag(item, select_customIdArray)) { - const itemkey = `item${i}tags.parent_id` - onCnfrmObj[itemkey] = - `items[${i}].tags.parent_id mismatches for Item ${itemId} in /${constants.SELECT} and /${constants.INIT}` - } + if (checkItemTag(item, select_customIdArray)) { + const itemkey = `item${i}tags.parent_id` + businessErrors[itemkey] = `items[${i}].tags.parent_id mismatches for Item ${itemId} in /${constants.SELECT} and /${constants.INIT}` + } - if (parentItemIdSet && item.parent_item_id && !parentItemIdSet.includes(item.parent_item_id)) { - const itemkey = `item_PrntItmId${i}` - onCnfrmObj[itemkey] = - `items[${i}].parent_item_id mismatches for Item ${itemId} in /${constants.ON_SEARCH} and /${constants.ON_INIT}` - } + if (parentItemIdSet && item.parent_item_id && !parentItemIdSet.includes(item.parent_item_id)) { + const itemkey = `item_PrntItmId${i}` + businessErrors[itemkey] = `items[${i}].parent_item_id mismatches for Item ${itemId} in /${constants.ON_SEARCH} and /${constants.ON_INIT}` + } - if (itemId in itemFlfllmnts) { - const validFfIds = Array.isArray(itemFlfllmnts[itemId]) - ? itemFlfllmnts[itemId] - : [itemFlfllmnts[itemId]]; + if (itemId in itemFlfllmnts) { + const validFfIds = Array.isArray(itemFlfllmnts[itemId]) ? itemFlfllmnts[itemId] : [itemFlfllmnts[itemId]] - if (!validFfIds.includes(on_confirm.items[i].fulfillment_id)) { - const itemkey = `item_FFErr${i}`; - onCnfrmObj[itemkey] = - `items[${i}].fulfillment_id (${on_confirm.items[i].fulfillment_id}) does not match any valid fulfillment_id for Item ${itemId} in /${constants.ON_SELECT}`; - } - } else { - const itemkey = `item_FFErr${i}`; - onCnfrmObj[itemkey] = `Item Id ${itemId} does not exist in /on_select`; + if (!validFfIds.includes(on_confirm.items[i].fulfillment_id)) { + const itemkey = `item_FFErr${i}` + businessErrors[itemkey] = `items[${i}].fulfillment_id (${on_confirm.items[i].fulfillment_id}) does not match any valid fulfillment_id for Item ${itemId} in /${constants.ON_SELECT}` } + } else { + const itemkey = `item_FFErr${i}` + businessErrors[itemkey] = `Item Id ${itemId} does not exist in /on_select` + } - if (itemId in itemsIdList) { - if (on_confirm.items[i].quantity.count != itemsIdList[itemId]) { - onCnfrmObj.countErr = `Warning: items[${i}].quantity.count for item ${itemId} mismatches with the items quantity selected in /${constants.SELECT}` - } + if (itemId in itemsIdList) { + if (on_confirm.items[i].quantity.count != itemsIdList[itemId]) { + businessErrors.countErr = `Warning: items[${i}].quantity.count for item ${itemId} mismatches with the items quantity selected in /${constants.SELECT}` } - - i++ } - } catch (error: any) { - logger.error( - `!!Error while comparing Item and Fulfillment Id in /${constants.ON_SELECT} and /${constants.CONFIRM}, ${error.stack}`, - ) + + i++ } + } catch (error: any) { + logger.error( + `!!Error while comparing Item and Fulfillment Id in /${constants.ON_SELECT} and /${constants.CONFIRM}, ${error.stack}`, + ) + businessErrors.items = `Error while comparing Item and Fulfillment Id in /${constants.ON_SELECT} and /${constants.CONFIRM}: ${error.message}` + } - try { - logger.info(`Storing fulfillment from ${constants.ON_CONFIRM}`) + try { + logger.info(`Storing fulfillment from ${constants.ON_CONFIRM}`) - const fulfillmentTypes = ['Delivery', 'Buyer-Delivery'] + const fulfillmentTypes = ['Delivery', 'Buyer-Delivery'] - const fulfillments = on_confirm.fulfillments || [] - const foundType:any = fulfillmentTypes.find(type => fulfillments.some((f: any) => f.type === type)) + const fulfillments = on_confirm.fulfillments || [] + const foundType: any = fulfillmentTypes.find((type) => fulfillments.some((f: any) => f.type === type)) + const matchingFulfillments = fulfillments.filter((f: any) => f.type === foundType) - const matchingFulfillments = fulfillments.filter((f: any) => f.type === foundType) + console.log('Matching fulfillments:', JSON.stringify(matchingFulfillments)) - console.log("Matching fulfillments:", JSON.stringify(matchingFulfillments)) + const validFulfillment = matchingFulfillments.find((f: any) => { + const startRange = f.start?.time?.range + const endRange = f.end?.time?.range + return startRange && endRange + }) - const validFulfillment = matchingFulfillments.find((f: any) => { - const startRange = f.start?.time?.range - const endRange = f.end?.time?.range - return startRange && endRange - }) + if (validFulfillment) { + const keyBase = foundType.replace(/[-\s]/g, '') + setValue(`${keyBase}Fulfillment`, validFulfillment) + setValue(`${keyBase}FulfillmentAction`, ApiSequence.ON_CONFIRM) + logger.info(`Stored valid fulfillment of type ${foundType}`) + } else { + logger.warn(`${foundType} fulfillment(s) found but no valid time range`) + } + } catch (error: any) { + logger.error(`Error while storing fulfillment: ${error.stack}`) + businessErrors.fulfillment_storage = `Error while storing fulfillment: ${error.message}` + } - if (validFulfillment) { - const keyBase = foundType.replace(/[-\s]/g, '') - setValue(`${keyBase}Fulfillment`, validFulfillment) - setValue(`${keyBase}FulfillmentAction`, ApiSequence.ON_CONFIRM) - logger.info(`Stored valid fulfillment of type ${foundType}`) - } else { - logger.warn(`${foundType} fulfillment(s) found but no valid time range`) + //Vehicle registeration for (Self-Pickup) Kerbside + const fulfillments = on_confirm.fulfillments + if (Array.isArray(fulfillments)) { + fulfillments.forEach((fulfillment, index) => { + const type = fulfillment.type + const category = fulfillment['@ondc/org/category'] + const vehicle = fulfillment.vehicle + const SELF_PICKUP = 'Self-Pickup' + const KERBSIDE = 'Kerbside' + + if (type === SELF_PICKUP && category === KERBSIDE) { + if (!vehicle) { + businessErrors[`fulfillment${index}_vehicle`] = + `Vehicle is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}` + } else if (!vehicle.registration) { + businessErrors[`fulfillment${index}_vehicle_registration`] = + `Vehicle registration is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}` + } + } else if (vehicle) { + businessErrors[`fulfillment${index}_vehicle`] = + `Vehicle should not be present in fulfillment ${index} with type ${type} and category ${category} in /${constants.CONFIRM}` } + }) + } - } catch (error: any) { - logger.error(`Error while storing fulfillment: ${error.stack}`) - } - - - - // if (on_confirm.state === 'Accepted') { - // try { - // // For Delivery Object - // const fulfillments = on_confirm.fulfillments - // if (fulfillments) { - // const deliveryFulfillment = fulfillments.find((f: any) => f.type === 'Delivery') - // if (!deliveryFulfillment.hasOwnProperty('provider_id')) { - // const key = `missingFulfillments` - // onCnfrmObj[key] = `provider_id must be present in ${ApiSequence.ON_CONFIRM} as order is accepted` - // } - - // const id = getProviderId(deliveryFulfillment) - // setValue('fulfillmentProviderId', id) - // } - // if (!fulfillments.length) { - // const key = `missingFulfillments` - // onCnfrmObj[key] = `missingFulfillments is mandatory for ${ApiSequence.ON_CONFIRM}` - // } else { - // const deliveryObjArr = _.filter(fulfillments, { type: 'Delivery' }) - // if (!deliveryObjArr.length) { - // onCnfrmObj[`message/order.fulfillments/`] = - // `Delivery fullfillment must be present in ${ApiSequence.ON_CONFIRM} if the Order.state is 'Accepted'` - // } else { - // const deliverObj = deliveryObjArr[0] - // delete deliverObj?.state - // delete deliverObj?.tags - // delete deliverObj?.start?.instructions - // delete deliverObj?.end?.instructions - // fulfillmentsItemsSet.add(deliverObj) - // } - // } - // } catch (error: any) { - // logger.error(`Error while checking Fulfillments Delivery Obj in /${ApiSequence.ON_CONFIRM}, ${error.stack}`) - // } - // } - //Vehicle registeration for (Self-Pickup) Kerbside - const fulfillments = on_confirm.fulfillments - if (Array.isArray(fulfillments)) { - fulfillments.forEach((fulfillment, index) => { - const type = fulfillment.type - const category = fulfillment['@ondc/org/category'] - const vehicle = fulfillment.vehicle - const SELF_PICKUP = 'Self-Pickup' - const KERBSIDE = 'Kerbside' - - if (type === SELF_PICKUP && category === KERBSIDE) { - if (!vehicle) { - onCnfrmObj[`fulfillment${index}_vehicle`] = - `Vehicle is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}` - } else if (!vehicle.registration) { - onCnfrmObj[`fulfillment${index}_vehicle_registration`] = - `Vehicle registration is required for fulfillment ${index} with type ${SELF_PICKUP} and category ${KERBSIDE} in /${constants.CONFIRM}` + try { + logger.info(`Checking for valid pan_id in provider_tax_number and tax_number in /on_confirm`) + const bpp_terms_obj: any = message.order.tags.filter((item: any) => { + return item?.code == 'bpp_terms' + })[0] + const list = bpp_terms_obj.list + const np_type_arr = list.filter((item: any) => item.code === 'np_type') + const accept_bap_terms = list.filter((item: any) => item.code === 'accept_bap_terms') + const np_type_on_search = getValue(`${ApiSequence.ON_SEARCH}np_type`) + let np_type = '' + + if (np_type_arr.length > 0) { + np_type = np_type_arr[0].value + } else { + const key = 'message.order.tags[0].list' + businessErrors[key] = `np_type not found in on_confirm` + } + + if (accept_bap_terms.length > 0) { + const key = 'message.order.tags[0].list' + businessErrors[key] = `accept_bap_terms is not required for now!` + } + + if (np_type && np_type != np_type_on_search) { + const key = 'message.order.tags[0].list' + businessErrors[key] = `np_type of on_search is not same to np_type of on_confirm` + } + + if (!_.isEmpty(bpp_terms_obj)) { + let tax_number = '' + let provider_tax_number = '' + list.map((item: any) => { + if (item.code == 'tax_number') { + if (item.value.length != 15) { + const key = `message.order.tags[0].list` + businessErrors[key] = `Number of digits in tax number in message.order.tags[0].list should be 15` + } else { + tax_number = item.value + } + } + if (item.code == 'provider_tax_number') { + if (item.value.length != 10) { + const key = `message.order.tags[0].list` + businessErrors[key] = `Number of digits in provider tax number in message.order.tags[0].list should be 10` + } else { + provider_tax_number = item.value } - } else if (vehicle) { - onCnfrmObj[`fulfillment${index}_vehicle`] = - `Vehicle should not be present in fulfillment ${index} with type ${type} and category ${category} in /${constants.CONFIRM}` } }) - } - try { - logger.info(`Checking for valid pan_id in provider_tax_number and tax_number in /on_confirm`) - const bpp_terms_obj: any = message.order.tags.filter((item: any) => { - return item?.code == 'bpp_terms' - })[0] - const list = bpp_terms_obj.list - const np_type_arr = list.filter((item: any) => item.code === 'np_type') - const accept_bap_terms = list.filter((item: any) => item.code === 'accept_bap_terms') - const np_type_on_search = getValue(`${ApiSequence.ON_SEARCH}np_type`) - let np_type = '' - - if (np_type_arr.length > 0) { - np_type = np_type_arr[0].value + if (tax_number.length == 0) { + logger.error(`tax_number must present in ${constants.ON_CONFIRM}`) + businessErrors['tax_number'] = `tax_number must be present for ${constants.ON_CONFIRM}` } else { - const key = 'message.order.tags[0].list' - onCnfrmObj[key] = `np_type not found in on_confirm` + const taxNumberPattern = new RegExp('^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}') + if (!taxNumberPattern.test(tax_number)) { + logger.error(`Invalid format for tax_number in ${constants.ON_INIT}`) + businessErrors.tax_number = `Invalid format for tax_number in ${constants.ON_CONFIRM}` + } } - if (accept_bap_terms.length > 0) { - const key = 'message.order.tags[0].list' - onCnfrmObj[key] = `accept_bap_terms is not required for now!` + if (provider_tax_number.length == 0) { + logger.error(`tax_number must present in ${constants.ON_CONFIRM}`) + businessErrors['provider_tax_number'] = `provider_tax_number must be present for ${constants.ON_CONFIRM}` + } else { + const taxNumberPattern = new RegExp('^[A-Z]{5}[0-9]{4}[A-Z]{1}') + if (!taxNumberPattern.test(provider_tax_number)) { + logger.error(`Invalid format for provider_tax_number in ${constants.ON_INIT}`) + businessErrors.provider_tax_number = `Invalid format for provider_tax_number in ${constants.ON_CONFIRM}` + } } - if (np_type && np_type != np_type_on_search) { - const key = 'message.order.tags[0].list' - onCnfrmObj[key] = `np_type of on_search is not same to np_type of on_confirm` + if (tax_number.length == 15 && provider_tax_number.length == 10) { + const pan_id = tax_number.slice(2, 12) + if (pan_id != provider_tax_number && np_type_on_search == 'ISN') { + businessErrors[`message.order.tags[0].list`] = + `Pan_id is different in tax_number and provider_tax_number in message.order.tags[0].list` + logger.error( + 'businessErrors[`message.order.tags[0].list`] = `Pan_id is different in tax_number and provider_tax_number in message.order.tags[0].list`', + ) + } else if (pan_id == provider_tax_number && np_type_on_search === 'MSN') { + businessErrors[`message.order.tags[0].list`] = + `Pan_id shouldn't be same in tax_number and provider_tax_number in message.order.tags[0].list` + logger.error( + "businessErrors[`message.order.tags[0].list`] = `Pan_id shoudn't be same in tax_number and provider_tax_number in message.order.tags[0].list`", + ) + } } + } + } catch (error: any) { + logger.error(`Error while comparing valid pan_id in tax_number and provider_tax_number`) + businessErrors.tax_info = `Error while comparing valid pan_id in tax_number and provider_tax_number: ${error.message}` + } - if (!_.isEmpty(bpp_terms_obj)) { - let tax_number = '' - let provider_tax_number = '' - list.map((item: any) => { - if (item.code == 'tax_number') { - if (item.value.length != 15) { - const key = `message.order.tags[0].list` - onCnfrmObj[key] = `Number of digits in tax number in message.order.tags[0].list should be 15` - } else { - tax_number = item.value - } - } - - if (item.code == 'provider_tax_number') { - if (item.value.length != 10) { - const key = `message.order.tags[0].list` - onCnfrmObj[key] = `Number of digits in provider tax number in message.order.tags[0].list should be 10` - } else { - provider_tax_number = item.value - } - } - }) + try { + logger.info(`Comparing timestamp of context and updatedAt for /${constants.ON_CONFIRM}`) + if (!_.isEqual(context.timestamp, on_confirm.updated_at)) { + const key = `invldUpdtdTmstp` + businessErrors[key] = `updated_at timestamp should be equal to context timestamp for /${constants.ON_CONFIRM}` + logger.error(`updated_at timestamp should be equal to context timestamp for /${constants.ON_CONFIRM}`) + } + } catch (error: any) { + logger.error(`!!Error while compairing updated_at timestamp with context timestamp for ${constants.ON_CONFIRM}`) + businessErrors.updated_at = `Error while compairing updated_at timestamp with context timestamp for ${constants.ON_CONFIRM}: ${error.message}` + } - if (tax_number.length == 0) { - logger.error(`tax_number must present in ${constants.ON_CONFIRM}`) - onCnfrmObj['tax_number'] = `tax_number must be present for ${constants.ON_CONFIRM}` - } else { - const taxNumberPattern = new RegExp('^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$') - if (!taxNumberPattern.test(tax_number)) { - logger.error(`Invalid format for tax_number in ${constants.ON_INIT}`) - onCnfrmObj.tax_number = `Invalid format for tax_number in ${constants.ON_CONFIRM}` - } - } + try { + logger.info(`Comparing billing object in ${constants.CONFIRM} and /${constants.ON_CONFIRM}`) + const billing = getValue('billing') - if (provider_tax_number.length == 0) { - logger.error(`tax_number must present in ${constants.ON_CONFIRM}`) - onCnfrmObj['provider_tax_number'] = `provider_tax_number must be present for ${constants.ON_CONFIRM}` - } else { - const taxNumberPattern = new RegExp('^[A-Z]{5}[0-9]{4}[A-Z]{1}$') - if (!taxNumberPattern.test(provider_tax_number)) { - logger.error(`Invalid format for provider_tax_number in ${constants.ON_INIT}`) - onCnfrmObj.provider_tax_number = `Invalid format for provider_tax_number in ${constants.ON_CONFIRM}` - } - } + const billingErrors = compareObjects(billing, on_confirm.billing) - if (tax_number.length == 15 && provider_tax_number.length == 10) { - const pan_id = tax_number.slice(2, 12) - if (pan_id != provider_tax_number && np_type_on_search == 'ISN') { - onCnfrmObj[`message.order.tags[0].list`] = - `Pan_id is different in tax_number and provider_tax_number in message.order.tags[0].list` - logger.error( - 'onCnfrmObj[`message.order.tags[0].list`] = `Pan_id is different in tax_number and provider_tax_number in message.order.tags[0].list`', - ) - } else if (pan_id == provider_tax_number && np_type_on_search === 'MSN') { - onCnfrmObj[`message.order.tags[0].list`] = - `Pan_id shouldn't be same in tax_number and provider_tax_number in message.order.tags[0].list` - logger.error( - "onCnfrmObj[`message.order.tags[0].list`] = `Pan_id shoudn't be same in tax_number and provider_tax_number in message.order.tags[0].list`", - ) - } - } + if (billingErrors) { + let i = 0 + const len = billingErrors.length + while (i < len) { + const key = `billingErr${i}` + businessErrors[key] = `${billingErrors[i]} when compared with init billing object` + i++ } - } catch (error: any) { - logger.error(`Error while comparing valid pan_id in tax_number and provider_tax_number`) } + } catch (error: any) { + logger.info(`!Error while comparing billing object in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) + businessErrors.billing = `Error while comparing billing object in /${constants.CONFIRM} and /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing timestamp of context and updatedAt for /${constants.ON_CONFIRM}`) - if (!_.isEqual(context.timestamp, on_confirm.updated_at)) { - const key = `invldUpdtdTmstp` - onCnfrmObj[key] = `updated_at timestamp should be equal to context timestamp for /${constants.ON_CONFIRM}` - logger.error(`updated_at timestamp should be equal to context timestamp for /${constants.ON_CONFIRM}`) + try { + logger.info(`Checking fulfillments objects in /${constants.ON_CONFIRM}`) + const itemFlfllmnts: any = getValue('itemFlfllmnts') + let i = 0 + const len = on_confirm.fulfillments.length + while (i < len) { + //Comparing fulfillment Ids + if (on_confirm.fulfillments[i].id) { + const id = on_confirm.fulfillments[i].id + if (!Object.values(itemFlfllmnts).includes(id)) { + const key = `ffID${id}` + //MM->Mismatch + businessErrors[key] = `fulfillment id ${id} does not exist in /${constants.ON_SELECT}` + } + } else { + businessErrors.ffId = `fulfillments[${i}].id is missing in /${constants.ON_CONFIRM}` } - } catch (error: any) { - logger.error(`!!Error while compairing updated_at timestamp with context timestamp for ${constants.ON_CONFIRM}`) - } - - try { - logger.info(`Comparing billing object in ${constants.CONFIRM} and /${constants.ON_CONFIRM}`) - const billing = getValue('billing') - - const billingErrors = compareObjects(billing, on_confirm.billing) - if (billingErrors) { - let i = 0 - const len = billingErrors.length - while (i < len) { - const key = `billingErr${i}` - onCnfrmObj[key] = `${billingErrors[i]} when compared with init billing object` - i++ - } + if (!on_confirm.fulfillments[i].type) { + const key = `ffID type` + businessErrors[key] = `fulfillment type does not exist in /${constants.ON_SELECT}` } - } catch (error: any) { - logger.info(`!Error while comparing billing object in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) - } - try { - logger.info(`Checking fulfillments objects in /${constants.ON_CONFIRM}`) - const itemFlfllmnts: any = getValue('itemFlfllmnts') - let i = 0 - const len = on_confirm.fulfillments.length - while (i < len) { - //Comparing fulfillment Ids - if (on_confirm.fulfillments[i].id) { - const id = on_confirm.fulfillments[i].id - if (!Object.values(itemFlfllmnts).includes(id)) { - const key = `ffID${id}` - //MM->Mismatch - onCnfrmObj[key] = `fulfillment id ${id} does not exist in /${constants.ON_SELECT}` + const ffId = on_confirm.fulfillments[i].id || '' + if (getValue(`${ffId}_tracking`)) { + if (on_confirm.fulfillments[i].tracking === false || on_confirm.fulfillments[i].tracking === true) { + if (getValue(`${ffId}_tracking`) != on_confirm.fulfillments[i].tracking) { + logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) + businessErrors['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` } } else { - onCnfrmObj.ffId = `fulfillments[${i}].id is missing in /${constants.ON_CONFIRM}` - } - - if (!on_confirm.fulfillments[i].type) { - const key = `ffID type` - onCnfrmObj[key] = `fulfillment type does not exist in /${constants.ON_SELECT}` + logger.info(`Tracking must be present for fulfillment ID: ${ffId} in boolean form`) + businessErrors['ffTracking'] = `Tracking must be present for fulfillment ID: ${ffId} in boolean form` } + } - const ffId = on_confirm.fulfillments[i].id || '' - if (getValue(`${ffId}_tracking`)) { - if (on_confirm.fulfillments[i].tracking === false || on_confirm.fulfillments[i].tracking === true) { - if (getValue(`${ffId}_tracking`) != on_confirm.fulfillments[i].tracking) { - logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) - onCnfrmObj['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` - } - } else { - logger.info(`Tracking must be present for fulfillment ID: ${ffId} in boolean form`) - onCnfrmObj['ffTracking'] = `Tracking must be present for fulfillment ID: ${ffId} in boolean form` - } - } + logger.info('Checking the fulfillments state') - logger.info('Checking the fulfillments state') + const ffDesc = on_confirm.fulfillments[i].state.descriptor - const ffDesc = on_confirm.fulfillments[i].state.descriptor + const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false + setValue(`ffIdPrecancel`, ffDesc?.code) + if (!ffStateCheck) { + const key = `ffState${i}` + businessErrors[key] = `default fulfillments state is missing in /${constants.ON_CONFIRM}` + } - const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false - setValue(`ffIdPrecancel`, ffDesc?.code) - if (!ffStateCheck) { - const key = `ffState${i}` - onCnfrmObj[key] = `default fulfillments state is missing in /${constants.ON_CONFIRM}` + if (on_confirm.fulfillments[i].type === 'Self-Pickup') { + if (!on_confirm.fulfillments[i].start) { + businessErrors.ffstart = `fulfillments[${i}] start location is mandatory` } - - if (on_confirm.fulfillments[i].type === "Self-Pickup") { - if (!on_confirm.fulfillments[i].start) { - onCnfrmObj.ffstart = `fulfillments[${i}] start location is mandatory` - } + } else { + if (!on_confirm.fulfillments[i].start || !on_confirm.fulfillments[i].end) { + businessErrors.ffstartend = `fulfillments[${i}] start and end locations are mandatory` } + } - else { - if(!on_confirm.fulfillments[i].start || !on_confirm.fulfillments[i].end){ - onCnfrmObj.ffstartend = `fulfillments[${i}] start and end locations are mandatory` - } + try { + if (!compareCoordinates(on_confirm.fulfillments[i].start.location.gps, getValue('providerGps'))) { + businessErrors.sellerGpsErr = `store gps location /fulfillments[${i}]/start/location/gps can't change` } + } catch (error: any) { + logger.error(`!!Error while checking store location in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.sellerGpsErr = `Error while checking store location in /${constants.ON_CONFIRM}: ${error.message}` + } - try { - if (!compareCoordinates(on_confirm.fulfillments[i].start.location.gps, getValue('providerGps'))) { - onCnfrmObj.sellerGpsErr = `store gps location /fulfillments[${i}]/start/location/gps can't change` - } - } catch (error: any) { - logger.error(`!!Error while checking store location in /${constants.ON_CONFIRM}, ${error.stack}`) + try { + if (!getValue('providerName')) { + businessErrors.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_CONFIRM}` + } else if ( + !_.isEqual(on_confirm.fulfillments[i].start.location.descriptor.name, getValue('providerName')) + ) { + businessErrors.sellerNameErr = `store name /fulfillments[${i}]/start/location/descriptor/name can't change` } - - try { - if (!getValue('providerName')) { - onCnfrmObj.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_CONFIRM}` - } else if (!_.isEqual(on_confirm.fulfillments[i].start.location.descriptor.name, getValue('providerName'))) { - onCnfrmObj.sellerNameErr = `store name /fulfillments[${i}]/start/location/descriptor/name can't change` - } - } catch (error: any) { - logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) + } catch (error: any) { + logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) + businessErrors.sellerNameErr = `Error while checking store name in /${constants.ON_CONFIRM}: ${error.message}` + } + if (on_confirm.fulfillments[i].type !== 'Self-Pickup') { + if (!_.isEqual(on_confirm.fulfillments[i].end.location.gps, getValue('buyerGps'))) { + businessErrors.buyerGpsErr = `fulfillments[${i}].end.location gps is not matching with gps in /select` } - if (on_confirm.fulfillments[i].type !== "Self-Pickup") { - if (!_.isEqual(on_confirm.fulfillments[i].end.location.gps, getValue('buyerGps'))) { - onCnfrmObj.buyerGpsErr = `fulfillments[${i}].end.location gps is not matching with gps in /select` - } - if (!_.isEqual(on_confirm.fulfillments[i].end.location.address.area_code, getValue('buyerAddr'))) { - onCnfrmObj.gpsErr = `fulfillments[${i}].end.location.address.area_code is not matching with area_code in /select` - } + if (!_.isEqual(on_confirm.fulfillments[i].end.location.address.area_code, getValue('buyerAddr'))) { + businessErrors.gpsErr = `fulfillments[${i}].end.location.address.area_code is not matching with area_code in /select` } - - i++ } - } catch (error: any) { - logger.error(`!!Error while checking fulfillments object in /${constants.ON_CONFIRM}, ${error.stack}`) - } - try { - logger.info(`Comparing /${constants.ON_CONFIRM} quoted Price and Payment Params amount`) - setValue('quotePrice', on_confirm.quote.price.value) - if (parseFloat(on_confirm.payment.params.amount) != parseFloat(on_confirm.quote.price.value)) { - onCnfrmObj.onConfirmedAmount = `Quoted price (/${constants.ON_CONFIRM}) doesn't match with the amount in payment.params` - } - } catch (error: any) { - logger.error( - `!!Error while Comparing /${constants.ON_CONFIRM} quoted Price and Payment Params amount, ${error.stack}`, - ) + i++ } + } catch (error: any) { + logger.error(`!!Error while checking fulfillments object in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.fulfillments_object = `Error while checking fulfillments object in /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Checking quote breakup prices for /${constants.ON_CONFIRM}`) - if (!sumQuoteBreakUp(on_confirm.quote)) { - const key = `invldPrices` - onCnfrmObj[key] = `item quote breakup prices for ${constants.ON_CONFIRM} should be equal to the total price.` - logger.error(`item quote breakup prices for ${constants.ON_CONFIRM} should be equal to the total price`) - } - const collect_payment = getValue('collect_payment') - if (flow === FLOW.FLOW0099 || collect_payment === 'N' || flow === OFFERSFLOW.FLOW0098) { - let offers = on_confirm.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') - if (offers.length === 0) { - onCnfrmObj['offer-not-found'] = `Offer is required for the flow: ${flow}` - } else if (offers.length > 0) { - const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) - offers.forEach((offer: any, index: number) => { - const providerOffer = providerOffers?.find( - (providedOffer: any) => providedOffer?.id.toLowerCase() === offer['@ondc/org/item_id'].toLowerCase(), - ) - console.log('providerOffer in select call', JSON.stringify(providerOffer)) - - if (!providerOffer) { - onCnfrmObj[`offer[${index}]`] = `Offer with id ${offer.id} is not available for the provider.` - return - } - const offerType = providerOffer.descriptor.code - if (offerType === 'financing') { - const finance_tags = offer.item.tags.find((tag: any) => tag.code === 'finance_terms') - const finance_txn = offer.item.tags.find((tag: any) => tag.code === 'finance_txn') - if (finance_tags) { - const on_init_finance_tags = getValue(`finance_terms${constants.ON_INIT}`) - const financeTagsError = compareFinanceTermsTags(on_init_finance_tags,finance_tags) - if (financeTagsError) { - let i = 0 - const len = financeTagsError.length - while (i < len) { - const key = `financeTagsError${i}` - onCnfrmObj[key] = `${financeTagsError[i]}` - i++ - } + try { + logger.info(`Comparing /${constants.ON_CONFIRM} quoted Price and Payment Params amount`) + setValue('quotePrice', on_confirm.quote.price.value) + if (parseFloat(on_confirm.payment.params.amount) != parseFloat(on_confirm.quote.price.value)) { + businessErrors.onConfirmedAmount = `Quoted price (/${constants.ON_CONFIRM}) doesn't match with the amount in payment.params` + } + } catch (error: any) { + logger.error( + `!!Error while Comparing /${constants.ON_CONFIRM} quoted Price and Payment Params amount, ${error.stack}`, + ) + businessErrors.onConfirmedAmount = `Error while Comparing /${constants.ON_CONFIRM} quoted Price and Payment Params amount: ${error.message}` + } + + try { + logger.info(`Checking quote breakup prices for /${constants.ON_CONFIRM}`) + if (!sumQuoteBreakUp(on_confirm.quote)) { + const key = `invldPrices` + businessErrors[key] = `item quote breakup prices for ${constants.ON_CONFIRM} should be equal to the total price.` + logger.error(`item quote breakup prices for ${constants.ON_CONFIRM} should be equal to the total price`) + } + const collect_payment = getValue('collect_payment') + if (flow === FLOW.FLOW0099 || collect_payment === 'N' || flow === OFFERSFLOW.FLOW0098) { + let offers = on_confirm.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') + if (offers.length === 0) { + businessErrors['offer-not-found'] = `Offer is required for the flow: ${flow}` + } else if (offers.length > 0) { + const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) + offers.forEach((offer: any, index: number) => { + const providerOffer = providerOffers?.find( + (providedOffer: any) => providedOffer?.id.toLowerCase() === offer['@ondc/org/item_id'].toLowerCase(), + ) + console.log('providerOffer in select call', JSON.stringify(providerOffer)) + + if (!providerOffer) { + businessErrors[`offer[${index}]`] = `Offer with id ${offer.id} is not available for the provider.` + return + } + const offerType = providerOffer.descriptor.code + if (offerType === 'financing') { + const finance_tags = offer.item.tags.find((tag: any) => tag.code === 'finance_terms') + const finance_txn = offer.item.tags.find((tag: any) => tag.code === 'finance_txn') + if (finance_tags) { + const on_init_finance_tags = getValue(`finance_terms${constants.ON_INIT}`) + const financeTagsError = compareFinanceTermsTags(on_init_finance_tags, finance_tags) + if (financeTagsError) { + let i = 0 + const len = financeTagsError.length + while (i < len) { + const key = `financeTagsError${i}` + businessErrors[key] = `${financeTagsError[i]}` + i++ } } - if(finance_txn){ - const financeTxnError = validateFinanceTxnTag(finance_txn) - if (financeTxnError) { - let i = 0 - const len = financeTxnError.length - while (i < len) { - const key = `financeTagsError${i}` - onCnfrmObj[key] = `${financeTxnError[i]}` - i++ - } + } + if (finance_txn) { + const financeTxnError = validateFinanceTxnTag(finance_txn) + if (financeTxnError) { + let i = 0 + const len = financeTxnError.length + while (i < len) { + const key = `financeTagsError${i}` + businessErrors[key] = `${financeTxnError[i]}` + i++ } } } - }) - } + } + }) } - } catch (error: any) { - logger.error(`!!Error while Comparing Quote object for /${constants.ON_CONFIRM}`) } + } catch (error: any) { + logger.error(`!!Error while Comparing Quote object for /${constants.ON_CONFIRM}`) + businessErrors.quote = `Error while Comparing Quote object for /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing Quote object for /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) + try { + logger.info(`Comparing Quote object for /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) - const on_select_quote: any = getValue('quoteObj') - if (flow !== FLOW.FLOW0099 && flow != OFFERSFLOW.FLOW0098) { + const on_select_quote: any = getValue('quoteObj') + if (flow !== FLOW.FLOW0099 && flow != OFFERSFLOW.FLOW0098) { const quoteErrors = compareQuoteObjects( on_select_quote, on_confirm.quote, constants.ON_CONFIRM, constants.ON_SELECT, ) - const hasItemWithQuantity = _.some(on_confirm.quote.breakup, (item) => _.has(item, 'item.quantity')) if (hasItemWithQuantity) { const key = `quantErr` - onCnfrmObj[key] = + businessErrors[key] = `Extra attribute Quantity provided in quote object i.e not supposed to be provided after on_select so invalid quote object` } else if (quoteErrors) { let i = 0 const len = quoteErrors.length while (i < len) { const key = `quoteErr${i}` - onCnfrmObj[key] = `${quoteErrors[i]}` + businessErrors[key] = `${quoteErrors[i]}` i++ } } } - } catch (error: any) { - logger.error(`!!Error while comparing quote in /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) - } - - try { - logger.info(`Comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}`) - const oninitQuotePrice: any = getValue('initQuotePrice') - const onConfirmQuotePrice = parseFloat(on_confirm.quote.price.value) - setValue(`${constants.ON_CONFIRM}/quote`,on_confirm.quote) - - logger.info(`Comparing quote prices of /${constants.ON_INIT} and /${constants.CONFIRM}`) - if (oninitQuotePrice != onConfirmQuotePrice) { - logger.info( - `order quote price in /${constants.CONFIRM} is not equal to the quoted price in /${constants.ON_INIT}`, - ) - onCnfrmObj.quoteErr = `Quoted Price in /${constants.CONFIRM} INR ${onConfirmQuotePrice} does not match with the quoted price in /${constants.ON_INIT} INR ${oninitQuotePrice}` - } - setValue('quotePrice', onConfirmQuotePrice) - } catch (error: any) { - logger.error(`!!Error while comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}`) - } + } catch (error: any) { + logger.error(`!!Error while comparing quote in /${constants.ON_SELECT} and /${constants.ON_CONFIRM}`) + businessErrors.quote_comparison = `Error while comparing quote in /${constants.ON_SELECT} and /${constants.ON_CONFIRM}: ${error.message}` + } - try { - logger.info(`Comparing payment object in /${constants.CONFIRM} & /${constants.ON_CONFIRM}`) + try { + logger.info(`Comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}`) + const oninitQuotePrice: any = getValue('initQuotePrice') + const onConfirmQuotePrice = parseFloat(on_confirm.quote.price.value) + setValue(`${constants.ON_CONFIRM}/quote`, on_confirm.quote) - if (!_.isEqual(getValue('cnfrmpymnt'), on_confirm.payment)) { - onCnfrmObj.pymntObj = `payment object mismatches in /${constants.CONFIRM} & /${constants.ON_CONFIRM}` - } - } catch (error: any) { - logger.error( - `!!Error while comparing payment object in /${constants.CONFIRM} & /${constants.ON_CONFIRM}, ${error.stack}`, + logger.info(`Comparing quote prices of /${constants.ON_INIT} and /${constants.CONFIRM}`) + if (oninitQuotePrice != onConfirmQuotePrice) { + logger.info( + `order quote price in /${constants.CONFIRM} is not equal to the quoted price in /${constants.ON_INIT}`, ) + businessErrors.quoteErr = `Quoted Price in /${constants.CONFIRM} INR ${onConfirmQuotePrice} does not match with the quoted price in /${constants.ON_INIT} INR ${oninitQuotePrice}` } + setValue('quotePrice', onConfirmQuotePrice) + } catch (error: any) { + logger.error(`!!Error while comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}`) + businessErrors.price_comparison = `Error while comparing order price value in /${constants.ON_INIT} and /${constants.CONFIRM}: ${error.message}` + } - try { - logger.info(`Checking Buyer App finder fee amount in /${constants.ON_CONFIRM}`) - const buyerFF: any = getValue(`${ApiSequence.SEARCH}_buyerFF`) - if ( - on_confirm.payment['@ondc/org/buyer_app_finder_fee_amount'] && - parseFloat(on_confirm.payment['@ondc/org/buyer_app_finder_fee_amount']) != buyerFF - ) { - onCnfrmObj.buyerFF = `Buyer app finder fee can't change in /${constants.ON_CONFIRM}` - logger.info(`Buyer app finder fee can't change in /${constants.ON_CONFIRM}`) - } - } catch (error: any) { - logger.info(`!Error while comparing buyer app finder fee in /${constants.ON_CONFIRM}, ${error.stack}`) + try { + logger.info(`Comparing payment object in /${constants.CONFIRM} & /${constants.ON_CONFIRM}`) + + if (!_.isEqual(getValue('cnfrmpymnt'), on_confirm.payment)) { + businessErrors.pymntObj = `payment object mismatches in /${constants.CONFIRM} & /${constants.ON_CONFIRM}` } + } catch (error: any) { + logger.error( + `!!Error while comparing payment object in /${constants.CONFIRM} & /${constants.ON_CONFIRM}, ${error.stack}`, + ) + businessErrors.payment_object = `Error while comparing payment object in /${constants.CONFIRM} & /${constants.ON_CONFIRM}: ${error.message}` + } - const list_ON_INIT: any = getValue('list_ON_INIT') - let ON_INIT_val: string + try { + logger.info(`Checking Buyer App finder fee amount in /${constants.ON_CONFIRM}`) + const buyerFF: any = getValue(`${ApiSequence.SEARCH}_buyerFF`) + if ( + on_confirm.payment['@ondc/org/buyer_app_finder_fee_amount'] && + parseFloat(on_confirm.payment['@ondc/org/buyer_app_finder_fee_amount']) != buyerFF + ) { + businessErrors.buyerFF = `Buyer app finder fee can't change in /${constants.ON_CONFIRM}` + logger.info(`Buyer app finder fee can't change in /${constants.ON_CONFIRM}`) + } + } catch (error: any) { + logger.info(`!Error while comparing buyer app finder fee in /${constants.ON_CONFIRM}, ${error.stack}`) + businessErrors.buyer_app_finder_fee = `Error while comparing buyer app finder fee in /${constants.ON_CONFIRM}: ${error.message}` + } + + const list_ON_INIT: any = getValue('list_ON_INIT') + let ON_INIT_val: string + if (list_ON_INIT) { list_ON_INIT.map((data: any) => { if (data.code == 'tax_number') { ON_INIT_val = data.value } }) + } - try { - logger.info(`Checking if tax_number in bpp_terms in ON_CONFIRM and ON_INIT is same`) - let list_ON_CONFIRM: any + try { + logger.info(`Checking if tax_number in bpp_terms in ON_CONFIRM and ON_INIT is same`) + let list_ON_CONFIRM: any + if (message.order.tags) { message.order.tags.forEach((data: any) => { if (data.code == 'bpp_terms') { list_ON_CONFIRM = data.list } }) - if (!list_ON_CONFIRM.some((data: any) => data.code == 'np_type')) { - onCnfrmObj['message/order/tags/bpp_terms/np_type'] = - `np_type is missing in message/order/tags/bpp_terms for ON_CONFIRM` - } + } + if (list_ON_CONFIRM && !list_ON_CONFIRM.some((data: any) => data.code == 'np_type')) { + businessErrors['message/order/tags/bpp_terms/np_type'] = + `np_type is missing in message/order/tags/bpp_terms for ON_CONFIRM` + } + if (list_ON_CONFIRM) { list_ON_CONFIRM.map((data: any) => { if (data.code == 'tax_number') { if (data.value != ON_INIT_val) { - onCnfrmObj['message/order/tags/bpp_terms'] = + businessErrors['message/order/tags/bpp_terms'] = `Value of tax Number mismatched in message/order/tags/bpp_terms for ON_INIT and ON_CONFIRM` } } }) - } catch (error: any) { - logger.error(`Error while matching the tax_number in ON_CONFIRM and ON_INIT`) } + } catch (error: any) { + logger.error(`Error while matching the tax_number in ON_CONFIRM and ON_INIT`) + businessErrors.tax_number_match = `Error while matching the tax_number in ON_CONFIRM and ON_INIT: ${error.message}` + } - try { - logger.info(`Comparing tags in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) - const confirm_tags: any[] | any = getValue('confirm_tags') - if (on_confirm.tags) { - const bap_terms = areGSTNumbersMatching(confirm_tags, on_confirm.tags, 'bap_terms') - - if (bap_terms === false) { - onCnfrmObj.tags_bap_terms = `Tags should have same and valid gst_number as passed in /${constants.CONFIRM}` - } + try { + logger.info(`Comparing tags in /${constants.CONFIRM} and /${constants.ON_CONFIRM}`) + const confirm_tags: any[] | any = getValue('confirm_tags') + if (on_confirm.tags) { + const bap_terms = areGSTNumbersMatching(confirm_tags, on_confirm.tags, 'bap_terms') + if (bap_terms === false) { + businessErrors.tags_bap_terms = `Tags should have same and valid gst_number as passed in /${constants.CONFIRM}` + } - const bpp_terms = areGSTNumbersMatching(confirm_tags, on_confirm.tags, 'bpp_terms') - if (bpp_terms === false) { - onCnfrmObj.tags_bpp_terms = `Tags should have same and valid gst_number as passed in /${constants.CONFIRM}` - } + const bpp_terms = areGSTNumbersMatching(confirm_tags, on_confirm.tags, 'bpp_terms') + if (bpp_terms === false) { + businessErrors.tags_bpp_terms = `Tags should have same and valid gst_number as passed in /${constants.CONFIRM}` } - } catch (error: any) { - logger.error( - `!!Error while Comparing tags in /${constants.CONFIRM} and /${constants.ON_CONFIRM} + } + } catch (error: any) { + logger.error( + `!!Error while Comparing tags in /${constants.CONFIRM} and /${constants.ON_CONFIRM} ${error.stack}`, - ) + ) + businessErrors.tags_comparison = `Error while Comparing tags in /${constants.CONFIRM} and /${constants.ON_CONFIRM}: ${error.message}` + } + + try { + logger.info(`Checking if transaction_id is present in message.order.payment`) + const payment = on_confirm.payment + const status = payment_status(payment, flow) + if (!status) { + businessErrors['message/order/transaction_id'] = `Transaction_id missing in message/order/payment` } + } catch (err: any) { + logger.error(`Error while checking transaction is in message.order.payment`) + businessErrors.transaction_id_payment = `Error while checking transaction_id in message.order.payment: ${err.message}` + } - try { - logger.info(`Checking if transaction_id is present in message.order.payment`) + try { + if (flow === FLOW.FLOW012) { + logger.info('Payment status check in on confirm call') const payment = on_confirm.payment - const status = payment_status(payment, flow) - if (!status) { - onCnfrmObj['message/order/transaction_id'] = `Transaction_id missing in message/order/payment` + const confirmPaymentStatus = getValue('confirmPaymentStatus') + if (payment.status !== confirmPaymentStatus) { + const key = `missmatchStatus` + businessErrors[key] = `payment status in /${constants.ON_CONFIRM} must be same as in /${constants.CONFIRM} ` + } + if (payment.status !== PAYMENT_STATUS.NOT_PAID) { + logger.error( + `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`, + ) + businessErrors.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` } - } catch (err: any) { - logger.error(`Error while checking transaction is in message.order.payment`) } - try { - if (flow === FLOW.FLOW012) { - logger.info('Payment status check in on confirm call') - const payment = on_confirm.payment - const confirmPaymentStatus = getValue('confirmPaymentStatus') - if (payment.status !== confirmPaymentStatus) { - const key = `missmatchStatus` - onCnfrmObj[key] = `payment status in /${constants.ON_CONFIRM} must be same as in /${constants.CONFIRM} ` - } - if (payment.status !== PAYMENT_STATUS.NOT_PAID) { - logger.error( - `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`, - ) - onCnfrmObj.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` + } catch (err: any) { + logger.error('Error while checking payment in message/order/payment: ' + err.message) + businessErrors.payment_status = `Error while checking payment in message/order/payment: ${err.message}` + } + + try { + logger.info(`Checking if list provided in bpp_terms is same as provided in ${constants.ON_INIT} `) + const tags = on_confirm.tags + + for (const tag of tags) { + if (tag.code === 'bpp_terms') { + const result = compareLists(tag.list, list_ON_INIT) + if (result.length > 0) { + businessErrors['message/order/tags/bpp_terms'] = + `List of bpp_terms mismatched in message/order/tags/bpp_terms for ${constants.ON_INIT} and ${constants.ON_CONFIRM} here ${result}` } } - } catch (err: any) { - logger.error('Error while checking payment in message/order/payment: ' + err.message) } + } catch (err: any) { + logger.error( + `Error while Checking if list provided in bpp_terms is same as provided in ${constants.ON_INIT}, ${err.stack} `, + ) + businessErrors.bpp_terms_list = `Error while Checking if list provided in bpp_terms is same as provided in ${constants.ON_INIT}: ${err.message}` + } - try { - logger.info(`Checking if list provided in bpp_terms is same as provided in ${constants.ON_INIT} `) - const tags = on_confirm.tags - - for (const tag of tags) { - if (tag.code === 'bpp_terms') { - const result = compareLists(tag.list, list_ON_INIT) - if (result.length > 0) { - onCnfrmObj['message/order/tags/bpp_terms'] = - `List of bpp_terms mismatched in message/order/tags/bpp_terms for ${constants.ON_INIT} and ${constants.ON_CONFIRM} here ${result}` - } + try { + logger.info(`Checking if bap_terms is present in ${constants.ON_CONFIRM} `) + const tags = on_confirm.tags + + for (const tag of tags) { + if (tag.code === 'bap_terms') { + const hasStaticTerms = tag.list.some((item: { code: string }) => item.code === 'static_terms') + if (hasStaticTerms) { + businessErrors['message/order/tags/bap_terms/static_terms'] = + `static_terms is not required for now! in ${constants.ON_CONFIRM}` } } - } catch (err: any) { - logger.error( - `Error while Checking if list provided in bpp_terms is same as provided in ${constants.ON_INIT}, ${err.stack} `, - ) } + } catch (err: any) { + logger.error(`Error while Checking bap_terms in ${constants.ON_CONFIRM}, ${err.stack} `) + businessErrors.bap_terms = `Error while Checking bap_terms in ${constants.ON_CONFIRM}: ${err.message}` + } - try { - logger.info(`Checking if bap_terms is present in ${constants.ON_CONFIRM} `) - const tags = on_confirm.tags - - for (const tag of tags) { - if (tag.code === 'bap_terms') { - const hasStaticTerms = tag.list.some((item: { code: string }) => item.code === 'static_terms') - if (hasStaticTerms) { - onCnfrmObj['message/order/tags/bap_terms/static_terms'] = - `static_terms is not required for now! in ${constants.ON_CONFIRM}` - } - } + if (flow === FLOW.FLOW003) { + const fulfillmentId = getValue('fulfillmentId') + const slot = getValue('fulfillmentSlots') + const ele = on_confirm.fulfillments.find((ele: { id: any }): any => ele.id === fulfillmentId) + const item = slot.find((ele: { id: any }): any => ele.id === fulfillmentId) + if (!ele || !item) { + const key = `fulfillments missing` + businessErrors[key] = `fulfillments must be same as in /${constants.ON_INIT}` + } + if (item?.end?.time?.range && ele?.end?.time?.range) { + const itemRange = item.end.time.range + const eleRange = ele.end.time.range + if (itemRange.start !== eleRange.start && itemRange.end !== eleRange.end) { + const key = `slotsMismatch` + businessErrors[key] = `slots in fulfillments must be same as in /${constants.ON_INIT}` } - } catch (err: any) { - logger.error(`Error while Checking bap_terms in ${constants.ON_CONFIRM}, ${err.stack} `) } + } - if (flow === FLOW.FLOW003) { - const fulfillmentId = getValue('fulfillmentId') - const slot = getValue('fulfillmentSlots') - const ele = on_confirm.fulfillments.find((ele: { id: any }): any => ele.id === fulfillmentId) - const item = slot.find((ele: { id: any }): any => ele.id === fulfillmentId) - if (!ele || !item) { - const key = `fulfillments missing` - onCnfrmObj[key] = `fulfillments must be same as in /${constants.ON_INIT}` - } - if (item?.end?.time?.range && ele?.end?.time?.range) { - const itemRange = item.end.time.range - const eleRange = ele.end.time.range + try { + const credsWithProviderId = getValue('credsWithProviderId') + const providerId = on_confirm?.provider?.id + const confirmCreds = on_confirm?.provider?.creds + const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) - if (itemRange.start !== eleRange.start && itemRange.end !== eleRange.end) { - const key = `slotsMismatch` - onCnfrmObj[key] = `slots in fulfillments must be same as in /${constants.ON_INIT}` - } + const expectedCreds = found?.creds + if (flow === FLOW.FLOW017) { + if (!expectedCreds || !credsWithProviderId || !confirmCreds) { + businessErrors['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` + } else if (!deepCompare(expectedCreds, confirmCreds)) { + businessErrors['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` } } - - try { - const credsWithProviderId = getValue('credsWithProviderId') - const providerId = on_confirm?.provider?.id - const confirmCreds = on_confirm?.provider?.creds - const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) - - const expectedCreds = found?.creds - if (flow === FLOW.FLOW017) { - if (!expectedCreds || !credsWithProviderId || !confirmCreds) { - onCnfrmObj['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` - } else if (!deepCompare(expectedCreds, confirmCreds)) { - onCnfrmObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` + if (credsWithProviderId && confirmCreds) { + if (expectedCreds) { + if (!deepCompare(expectedCreds, confirmCreds)) { + businessErrors['MissingCreds'] = `Mismatch found in creds, it must be same as in /${constants.ON_SEARCH}` } } - if (credsWithProviderId && confirmCreds) { - if (expectedCreds) { - if (!deepCompare(expectedCreds, confirmCreds)) { - onCnfrmObj['MissingCreds'] = `Mismatch found in creds, it must be same as in /${constants.ON_SEARCH}` - } - } - } - } catch (err: any) { - logger.error(`Error while Checking creds in ${constants.ON_CONFIRM}, ${err.stack} `) } + } catch (err: any) { + logger.error(`Error while Checking creds in ${constants.ON_CONFIRM}, ${err.stack} `) + businessErrors.creds = `Error while Checking creds in ${constants.ON_CONFIRM}: ${err.message}` + } - if (flow === FLOW.FLOW01F) { - const bppTerms = getValue('bppTerms'); - const list = message.order.tags?.find((item: any) => item?.code === 'bpp_terms')?.list; - if (!deepCompare(list, bppTerms)) { - onCnfrmObj['missingbppTerms'] = `bppTerms must be same as provided in /${constants.ON_SEARCH} call`; + if (flow === FLOW.FLOW01F) { + const bppTerms = getValue('bppTerms') + const list = message.order.tags?.find((item: any) => item?.code === 'bpp_terms')?.list + if (!deepCompare(list, bppTerms)) { + businessErrors['missingbppTerms'] = `bppTerms must be same as provided in /${constants.ON_SEARCH} call` + } } -} - // Validate routing type for retail 1.2.5 - try { - logger.info(`Checking routing type in /${constants.ON_CONFIRM}`) - const orderState = getValue('orderState') - const domain = getValue('domain') - - if (orderState === 'Accepted') { - // Extract routing type from delivery fulfillment - const routingType = extractRoutingType(on_confirm.fulfillments) - - if (!routingType) { - onCnfrmObj.routingType = `Routing type tag is mandatory in delivery fulfillment when order.state is 'Accepted' in /${constants.ON_CONFIRM}` - } else if (!isValidRoutingType(routingType)) { - onCnfrmObj.routingType = `Invalid routing type '${routingType}'. Must be one of: P2P, P2H2P` - } else { - // Store routing type for subsequent validations - setValue('routingType', routingType) - logger.info(`Stored routing type: ${routingType} for domain: ${domain}`) - } + // Validate routing type for retail 1.2.5 + try { + logger.info(`Checking routing type in /${constants.ON_CONFIRM}`) + const orderState = getValue('orderState') + const domain = getValue('domain') + + if (orderState === 'Accepted') { + // Extract routing type from delivery fulfillment + const routingType = extractRoutingType(on_confirm.fulfillments) + + if (!routingType) { + businessErrors.routingType = `Routing type tag is mandatory in delivery fulfillment when order.state is 'Accepted' in /${constants.ON_CONFIRM}` + } else if (!isValidRoutingType(routingType)) { + businessErrors.routingType = `Invalid routing type '${routingType}'. Must be one of: P2P, P2H2P` + } else { + // Store routing type for subsequent validations + setValue('routingType', routingType) + logger.info(`Stored routing type: ${routingType} for domain: ${domain}`) } - } catch (error: any) { - logger.error(`Error while checking routing type in /${constants.ON_CONFIRM}: ${error.stack}`) } + } catch (error: any) { + logger.error(`Error while checking routing type in /${constants.ON_CONFIRM}: ${error.stack}`) + businessErrors.routingType = `Error while checking routing type in /${constants.ON_CONFIRM}: ${error.message}` + } + + const hasSchemaErrors = Object.keys(schemaErrors).length > 0 + const hasBusinessErrors = Object.keys(businessErrors).length > 0 + + if (schemaValidation === true) { + if (hasSchemaErrors) { + return { success: false, schemaErrors } + } else { + return { success: true, error: false } + } + } - return onCnfrmObj - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_CONFIRM} API`, JSON.stringify(err.stack)) + if (hasSchemaErrors || hasBusinessErrors) { + return { success: false, schemaErrors, businessErrors } + } else { + return { success: true, error: false } } } + + \ No newline at end of file diff --git a/utils/Retail_.1.2.5/Init/init.ts b/utils/Retail_.1.2.5/Init/init.ts index d6fca29c..8faf064a 100644 --- a/utils/Retail_.1.2.5/Init/init.ts +++ b/utils/Retail_.1.2.5/Init/init.ts @@ -1,3 +1,4 @@ + import _ from 'lodash' import constants, { ApiSequence } from '../../../constants' import { logger } from '../../../shared/logger' @@ -5,8 +6,9 @@ import { validateSchemaRetailV2, isObjectEmpty, checkContext, checkItemTag, chec import { getValue, setValue } from '../../../shared/dao' import { FLOW, OFFERSFLOW } from '../../../utils/enum' -export const checkInit = (data: any, msgIdSet: any, flow: string) => { +export const checkInit = (data: any, msgIdSet: any, flow: string, schemaValidation?: boolean, stateless?: boolean) => { const initObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.INIT]: 'JSON cannot be empty' } @@ -20,7 +22,9 @@ export const checkInit = (data: any, msgIdSet: any, flow: string) => { const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) const parentItemIdSet: any = getValue(`parentItemIdSet`) const select_customIdArray: any = getValue(`select_customIdArray`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.INIT, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.INIT, data) + : 'skip' const contextRes: any = checkContext(context, constants.INIT) @@ -30,17 +34,31 @@ export const checkInit = (data: any, msgIdSet: any, flow: string) => { if (checkBap) Object.assign(initObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(initObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(initObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } - if (_.isEqual(data.context, getValue(`domain`))) { - initObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + initObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } if (!contextRes?.valid) { Object.assign(initObj, contextRes.ERRORS) } + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(initObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: initObj } + } + // Merge schema and business errors into one object + const combinedErrors = { ...schemaErrors, ...initObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + setValue(`${ApiSequence.INIT}`, data) try { @@ -385,12 +403,12 @@ export const checkInit = (data: any, msgIdSet: any, flow: string) => { //Comparing fulfillment Ids const id = init.fulfillments[i].id if (id) { - if(flow === FLOW.FLOW002){ + if (flow === FLOW.FLOW002) { const onSelectSelfPickupFulfillment = getValue('selfPickupFulfillment') - if(id !== onSelectSelfPickupFulfillment.id){ + if (id !== onSelectSelfPickupFulfillment.id) { initObj['invldFlmntId'] = `Mismatch in on_init fulfillment.id: ${id} with on_select Self-Pickup fulfillment id: ${onSelectSelfPickupFulfillment.id} for Flow: ${FLOW.FLOW002}` } - if(init.fulfillments[i].type !== onSelectSelfPickupFulfillment.type){ + if (init.fulfillments[i].type !== onSelectSelfPickupFulfillment.type) { initObj['invldFlmntId'] = `Mismatch in on_init fulfillment.type: ${init.fulfillments[i].type} with on_select fulfillment.type: ${onSelectSelfPickupFulfillment.type} for Flow: ${FLOW.FLOW002}` } } @@ -467,10 +485,20 @@ export const checkInit = (data: any, msgIdSet: any, flow: string) => { initObj['bap_terms_value'] = "'finance_cost_value' must be a valid number" } } - } catch (error) {} + } catch (error) { } - return initObj - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.INIT} API`, err) + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(initObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: initObj } + } + const combinedErrors = { ...schemaErrors, ...initObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } catch (error: any) { + logger.error(`Error while checking for JSON structure and required fields for ${ApiSequence.INIT}: ${error.stack}`) + return { + error: `Error while checking for JSON structure and required fields for ${ApiSequence.INIT}: ${error.stack}`, + } } -} +} \ No newline at end of file diff --git a/utils/Retail_.1.2.5/Init/onInit.ts b/utils/Retail_.1.2.5/Init/onInit.ts index 624d1c2d..d2a44b19 100644 --- a/utils/Retail_.1.2.5/Init/onInit.ts +++ b/utils/Retail_.1.2.5/Init/onInit.ts @@ -19,9 +19,10 @@ import { getValue, setValue } from '../../../shared/dao' import { FLOW, OFFERSFLOW } from '../../enum' import { PAYMENT_COLLECTED_BY } from '../../../constants' -export const checkOnInit = (data: any, flow: string) => { +export const checkOnInit = (data: any, flow: string, schemaValidation?: boolean, stateless?: boolean) => { try { const onInitObj: any = {} + const schemaErrors: any = {} if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_INIT]: 'JSON cannot be empty' } } @@ -31,7 +32,9 @@ export const checkOnInit = (data: any, flow: string) => { return { missingFields: '/context, /message, /order or /message/order is missing or empty' } } - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_INIT, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_INIT, data) + : 'skip' const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) const contextRes: any = checkContext(context, constants.ON_INIT) const parentItemIdSet: any = getValue(`parentItemIdSet`) @@ -42,15 +45,33 @@ export const checkOnInit = (data: any, flow: string) => { if (checkBap) Object.assign(onInitObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(onInitObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(onInitObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } + if (!contextRes?.valid) { Object.assign(onInitObj, contextRes.ERRORS) } - if (_.isEqual(data.context, getValue(`domain`))) { - onInitObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onInitObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } + + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onInitObj).length > 0 + if (!hasSchema && !hasBusiness) return false + + // If schemaValidation parameter is provided, return separated errors + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onInitObj } + } + + // Otherwise return flat object + const combinedErrors = { ...schemaErrors, ...onInitObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } setValue(`${ApiSequence.ON_INIT}`, data) @@ -119,12 +140,12 @@ export const checkOnInit = (data: any, flow: string) => { try { logger.info(`Checking Cancellation terms for /${constants.ON_INIT}`) - + // Flow 01E requires cancellation_terms if (flow === FLOW.FLOW01E && !message.order.cancellation_terms) { onInitObj.cancellationTerms = `cancellation_terms are mandatory for flow ${FLOW.FLOW01E} in /${constants.ON_INIT}` } - + // Store cancellation_terms only if provided if (message.order.cancellation_terms) { setValue('OnInitCancellationTerms', message.order.cancellation_terms) @@ -287,9 +308,9 @@ export const checkOnInit = (data: any, flow: string) => { logger.info(`Validating fulfillments`) on_init?.fulfillments.forEach((fulfillment: any) => { const { type } = fulfillment - if(flow === FLOW.FLOW002){ - if(type !== "Self-Pickup"){ - onInitObj[`fulfillments[${fulfillment.id}]`] = `Fulfillment type should be 'Self-Pickup' for flow: ${flow}` + if (flow === FLOW.FLOW002) { + if (type !== "Self-Pickup") { + onInitObj[`fulfillments[${fulfillment.id}]`] = `Fulfillment type should be 'Self-Pickup' for flow: ${flow}` } } if (type == 'Delivery' || type == "Self-Pickup") { @@ -439,60 +460,60 @@ export const checkOnInit = (data: any, flow: string) => { if (on_init.payment.collected_by === PAYMENT_COLLECTED_BY.BAP) { onInitObj[`payment_collected_by`] = `payments.collected_by not allowed in ${constants.ON_INIT} when ${PAYMENT_COLLECTED_BY.BAP}` } - const collectedBy = on_init.payment.collected_by.includes(PAYMENT_COLLECTED_BY) - const collect_payment = getValue('collect_payment') - if (flow === FLOW.FLOW007 || collect_payment === 'Y' || flow === FLOW.FLOW012) { - if (collectedBy !== PAYMENT_COLLECTED_BY.BPP) { - onInitObj['invaliedPaymentCollectedBy'] = - `Payment collected_by should be ${PAYMENT_COLLECTED_BY.BPP} for flow: ${flow}` - } - const payment_uri = on_init.payment?.uri - if (!payment_uri || payment_uri.trim().length === 0) { - onInitObj['PaymentUri'] = - `Payment uri should be present when payment collect_by: ${PAYMENT_COLLECTED_BY.BPP} for flow: ${flow}` - } - const tags = on_init.payment.tags - const bpp_collect_tag = tags.find((tag: any) => tag.code === 'bpp_collect') - if (!bpp_collect_tag || !Array.isArray(bpp_collect_tag.list)) { - onInitObj['bpp_collect'] = "Tag 'bpp_collect' must have a valid 'list' array." - } + const collectedBy = on_init.payment.collected_by.includes(PAYMENT_COLLECTED_BY) + const collect_payment = getValue('collect_payment') + if (flow === FLOW.FLOW007 || collect_payment === 'Y' || flow === FLOW.FLOW012) { + if (collectedBy !== PAYMENT_COLLECTED_BY.BPP) { + onInitObj['invaliedPaymentCollectedBy'] = + `Payment collected_by should be ${PAYMENT_COLLECTED_BY.BPP} for flow: ${flow}` + } + const payment_uri = on_init.payment?.uri + if (!payment_uri || payment_uri.trim().length === 0) { + onInitObj['PaymentUri'] = + `Payment uri should be present when payment collect_by: ${PAYMENT_COLLECTED_BY.BPP} for flow: ${flow}` + } + const tags = on_init.payment.tags + const bpp_collect_tag = tags.find((tag: any) => tag.code === 'bpp_collect') + if (!bpp_collect_tag || !Array.isArray(bpp_collect_tag.list)) { + onInitObj['bpp_collect'] = "Tag 'bpp_collect' must have a valid 'list' array." + } - const successEntry = bpp_collect_tag.list.find((item: any) => item.code === 'success') - const errorEntry = bpp_collect_tag.list.find((item: any) => item.code === 'error') + const successEntry = bpp_collect_tag.list.find((item: any) => item.code === 'success') + const errorEntry = bpp_collect_tag.list.find((item: any) => item.code === 'error') - if (!successEntry || !['Y', 'N'].includes(successEntry.value)) { - onInitObj['bpp_collect_success'] = - "'success' code in 'bpp_collect' must be present and have value 'Y' or 'N'" - } + if (!successEntry || !['Y', 'N'].includes(successEntry.value)) { + onInitObj['bpp_collect_success'] = + "'success' code in 'bpp_collect' must be present and have value 'Y' or 'N'" + } - if (successEntry?.value === 'N') { - if (!errorEntry || typeof errorEntry.value !== 'string' || errorEntry.value.trim().length === 0) { - onInitObj['bpp_collect_error'] = "'error' must be present with a non-empty value when 'success' is 'N'" - } - } else if (successEntry?.value === 'Y' && errorEntry) { - onInitObj['bpp_collect_error_unexpected'] = "'error' should not be present when 'success' is 'Y'" + if (successEntry?.value === 'N') { + if (!errorEntry || typeof errorEntry.value !== 'string' || errorEntry.value.trim().length === 0) { + onInitObj['bpp_collect_error'] = "'error' must be present with a non-empty value when 'success' is 'N'" } - } else if (flow === FLOW.FLOW0099 || collect_payment === 'N' || flow === OFFERSFLOW.FLOW0098) { - let offers = on_init.quote.breakup.filter((item:any)=>item["@ondc/org/title_type"] === "offer") - if (offers.length === 0) { - onInitObj['offer-not-found'] = `Offer is required for the flow: ${flow}` - } else if (offers.length > 0) { - const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) - offers.forEach((offer: any, index: number) => { - const providerOffer = providerOffers?.find( - (providedOffer: any) => providedOffer?.id.toLowerCase() === offer["@ondc/org/item_id"].toLowerCase(), - ) - console.log('providerOffer in select call', JSON.stringify(providerOffer)) - - if (!providerOffer) { - onInitObj[`offer[${index}]`] = `Offer with id ${offer.id} is not available for the provider.` - return - } - const offerType = providerOffer.descriptor.code - if(offerType === "financing"){ - const finance_tags = offer.item.tags.find((tag:any)=>tag.code === "finance_terms") - if(finance_tags){ - setValue(`finance_terms${constants.ON_INIT}`,finance_tags) + } else if (successEntry?.value === 'Y' && errorEntry) { + onInitObj['bpp_collect_error_unexpected'] = "'error' should not be present when 'success' is 'Y'" + } + } else if (flow === FLOW.FLOW0099 || collect_payment === 'N' || flow === OFFERSFLOW.FLOW0098) { + let offers = on_init.quote.breakup.filter((item: any) => item["@ondc/org/title_type"] === "offer") + if (offers.length === 0) { + onInitObj['offer-not-found'] = `Offer is required for the flow: ${flow}` + } else if (offers.length > 0) { + const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) + offers.forEach((offer: any, index: number) => { + const providerOffer = providerOffers?.find( + (providedOffer: any) => providedOffer?.id.toLowerCase() === offer["@ondc/org/item_id"].toLowerCase(), + ) + console.log('providerOffer in select call', JSON.stringify(providerOffer)) + + if (!providerOffer) { + onInitObj[`offer[${index}]`] = `Offer with id ${offer.id} is not available for the provider.` + return + } + const offerType = providerOffer.descriptor.code + if (offerType === "financing") { + const finance_tags = offer.item.tags.find((tag: any) => tag.code === "finance_terms") + if (finance_tags) { + setValue(`finance_terms${constants.ON_INIT}`, finance_tags) const financeTagsError = validateFinanceTermsTag(finance_tags) if (financeTagsError) { let i = 0 @@ -503,13 +524,13 @@ export const checkOnInit = (data: any, flow: string) => { i++ } } - } } - }) + } + }) - } } - + } + } catch (error: any) { logger.error(`!!Error while checking /${constants.ON_INIT} Quoted Price and Net Price Breakup, ${error.stack}`) } @@ -554,18 +575,18 @@ export const checkOnInit = (data: any, flow: string) => { try { logger.info(`Checking Settlement basis in /${constants.ON_INIT}`) - if(on_init.payment.collected_by === "BPP"){ + if (on_init.payment.collected_by === "BPP") { const validSettlementBasis = ['delivery', 'shipment'] // Enums (as per API Contract) - const settlementBasis = on_init.payment['@ondc/org/settlement_basis'] - if(settlementBasis){ - if (!validSettlementBasis.includes(settlementBasis)) { - onInitObj.settlementBasis = `Invalid settlement basis in /${constants.ON_INIT}. Expected one of: ${validSettlementBasis.join(', ')}` - } - } - else{ - onInitObj.settlementBasis = `settlement basis is required in /${constants.ON_INIT} when payment.collected_by is : ${on_init.payment.collected_by}` - } + const settlementBasis = on_init.payment['@ondc/org/settlement_basis'] + if (settlementBasis) { + if (!validSettlementBasis.includes(settlementBasis)) { + onInitObj.settlementBasis = `Invalid settlement basis in /${constants.ON_INIT}. Expected one of: ${validSettlementBasis.join(', ')}` + } + } + else { + onInitObj.settlementBasis = `settlement basis is required in /${constants.ON_INIT} when payment.collected_by is : ${on_init.payment.collected_by}` + } } } catch (error: any) { logger.error(`!!Error while checking settlement basis in /${constants.ON_INIT}, ${error.stack}`) @@ -785,8 +806,20 @@ export const checkOnInit = (data: any, flow: string) => { onInitObj[key] = `all Items in /${constants.ON_INIT} must be available for cod ` } } - return onInitObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onInitObj).length > 0 + if (!hasSchema && !hasBusiness) return false + + // If schemaValidation parameter is provided, return separated errors + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onInitObj } + } + + // Otherwise return flat object + const combinedErrors = { ...schemaErrors, ...onInitObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_INIT} API`, err) + return { error: `Internal server error in /${constants.ON_INIT} validation` } } -} +} \ No newline at end of file diff --git a/utils/Retail_.1.2.5/RET11_onSearch/onSearch.ts b/utils/Retail_.1.2.5/RET11_onSearch/onSearch.ts index 210b1564..ba0d116d 100644 --- a/utils/Retail_.1.2.5/RET11_onSearch/onSearch.ts +++ b/utils/Retail_.1.2.5/RET11_onSearch/onSearch.ts @@ -35,2445 +35,2569 @@ import { FLOW } from '../../enum' // import { MenuTreeBuilder } from './fb_calculation/lower_upper_range/builder' // import { CatalogParser } from './fb_calculation/lower_upper_range/parser' -export const checkOnsearchFullCatalogRefresh = (data: any,flow:string) => { - if (!data || isObjectEmpty(data)) { - return { [ApiSequence.ON_SEARCH]: 'JSON cannot be empty' } - } - - const { message, context } = data - if (!message || !context || !message.catalog || isObjectEmpty(message) || isObjectEmpty(message.catalog)) { - return { missingFields: '/context, /message, /catalog or /message/catalog is missing or empty' } - } - - const schemaValidation = validateSchemaRetailV2('RET11', constants.ON_SEARCH, data) - - const contextRes: any = checkContext(context, constants.ON_SEARCH) - setValue(`${ApiSequence.ON_SEARCH}_context`, context) - setValue(`${ApiSequence.ON_SEARCH}_message`, message) +export const checkOnsearchFullCatalogRefresh = ( + data: any, + flow: string, + stateless: boolean = false, + schemaValidation?: boolean, +) => { let errorObj: any = {} - - if (schemaValidation !== 'error') { - Object.assign(errorObj, schemaValidation) - } - - try { - logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(getValue(`${ApiSequence.SEARCH}_msgId`), context.message_id)) { - errorObj[`${ApiSequence.ON_SEARCH}_msgId`] = - `Message Ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` - } - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_SEARCH}, ${error.stack}`) - } - - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } - - const checkBap = checkBppIdOrBapId(context.bap_id) - const checkBpp = checkBppIdOrBapId(context.bpp_id) - - if (checkBap) Object.assign(errorObj, { bap_id: 'context/bap_id should not be a url' }) - if (checkBpp) Object.assign(errorObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (!contextRes?.valid) { - Object.assign(errorObj, contextRes.ERRORS) - } - - validateBapUri(context.bap_uri, context.bap_id, errorObj) - validateBppUri(context.bpp_uri, context.bpp_id, errorObj) - - if (context.transaction_id == context.message_id) { - errorObj['on_search_full_catalog_refresh'] = - `Context transaction_id (${context.transaction_id}) and message_id (${context.message_id}) can't be the same.` - } - - setValue(`${ApiSequence.ON_SEARCH}`, data) - - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - - try { - logger.info(`Storing BAP_ID and BPP_ID in /${constants.ON_SEARCH}`) - setValue('bapId', context.bap_id) - setValue('bppId', context.bpp_id) - } catch (error: any) { - logger.error(`!!Error while storing BAP and BPP Ids in /${constants.ON_SEARCH}, ${error.stack}`) - } + const schemaErrors: Record = {} + const businessErrors: Record = {} try { - logger.info(`Comparing timestamp of /${ApiSequence.SEARCH} /${constants.ON_SEARCH}`) + logger.info(`Checking JSON structure and required fields for ${ApiSequence.ON_SEARCH} API with schemaValidation: ${schemaValidation}`) - if (searchContext.timestamp == context.timestamp) { - errorObj.tmstmp = `context/timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH} api cannot be same` + if (!data || isObjectEmpty(data)) { + errorObj[ApiSequence.ON_SEARCH] = 'JSON cannot be empty' + return errorObj } - } catch (error: any) { - logger.error(`!!Error while Comparing timestamp of /${ApiSequence.SEARCH} /${constants.ON_SEARCH}, ${error.stack}`) - } - try { - logger.info(`Comparing transaction Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(searchContext.transaction_id, context.transaction_id)) { - errorObj.transaction_id = `Transaction Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + const { message, context } = data + if (!message || !context || !message.catalog || isObjectEmpty(message) || isObjectEmpty(message.catalog)) { + errorObj['missingFields'] = '/context, /message, /catalog or /message/catalog is missing or empty' + return Object.keys(errorObj).length > 0 && errorObj } - } catch (error: any) { - logger.info( - `Error while comparing transaction ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } - try { - logger.info(`Checking customizations based on config.min and config.max values...`) - - const items = getValue('items') // Retrieve items from the catalog or message - - _.filter(items, (item) => { - const customGroup = item.custom_group // Assuming custom_group holds the configuration data - - // Check for minimum customizations - if (customGroup?.config?.min === 1) { - logger.info(`Checking min value for item id: ${item.id}`) - const defaultCustomizations = _.filter(item.customizations, (customization) => { - return customization.is_default // Check for default customizations - }) - - if (defaultCustomizations.length < 1) { - const key = `item${item.id}CustomGroup/min` - errorObj[key] = - `Item with id: ${item.id} must have at least one default customization as config.min is set to 1.` - } + // Schema validation - run unless explicitly disabled + if (schemaValidation !== false) { + const schemaDomain = context.domain.split(':')[1] + logger.info(`checkOnsearch: calling schema validation with domain=${schemaDomain}, api=${constants.ON_SEARCH}`) + const schemaValidation = validateSchemaRetailV2(schemaDomain, constants.ON_SEARCH, data) + logger.info(`checkOnsearch: schema validation result:`, schemaValidation) + + logger.info(`checkOnsearch: checking if schema validation !== 'error': ${schemaValidation !== 'error'}`) + if (schemaValidation !== 'error') { + logger.info(`checkOnsearch: adding schema errors to schemaErrors:`, schemaValidation) + Object.assign(schemaErrors, schemaValidation) + } else { + logger.info(`checkOnsearch: schema validation passed, no schema errors to add`) } + } - // Check for maximum customizations - if (customGroup?.config?.max === 2) { - logger.info(`Checking max value for item id: ${item.id}`) - - const customizationsCount = item.customizations.length + // Business logic validation - run unless explicitly schema-only + if (schemaValidation !== true) { + setValue(`${ApiSequence.ON_SEARCH}_context`, context) + setValue(`${ApiSequence.ON_SEARCH}_message`, message) - if (customizationsCount > 2) { - const key = `item${item.id}CustomGroup/max` - errorObj[key] = `Item with id: ${item.id} can have at most 2 customizations as config.max is set to 2.` + if (!stateless) { + try { + logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(getValue(`${ApiSequence.SEARCH}_msgId`), context.message_id)) { + businessErrors[`${ApiSequence.ON_SEARCH}_msgId`] = + `Message Ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + } + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.ON_SEARCH}, ${error.stack}`) } } - }) - } catch (error: any) { - logger.error(`Error while checking customizations for items, ${error.stack}`) - } - - // removed timestamp difference check - // try { - // logger.info(`Comparing timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - // const tmpstmp = searchContext?.timestamp - // if (_.gte(tmpstmp, context.timestamp)) { - // errorObj.tmpstmp = `Timestamp for /${constants.SEARCH} api cannot be greater than or equal to /${constants.ON_SEARCH} api` - // } else { - // const timeDiff = timeDifference(context.timestamp, tmpstmp) - // logger.info(timeDiff) - // if (timeDiff > 5000) { - // errorObj.tmpstmp = `context/timestamp difference between /${constants.ON_SEARCH} and /${constants.SEARCH} should be less than 5 sec` - // } - // } - // } catch (error: any) { - // logger.info( - // `Error while comparing timestamp for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - // ) - // } - try { - logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(searchContext.message_id, context.message_id)) { - errorObj.message_id = `Message Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` - } - } catch (error: any) { - logger.info( - `Error while comparing message ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } - - const onSearchCatalog: any = message.catalog - const onSearchFFIdsArray: any = [] - const prvdrsId = new Set() - const prvdrLocId = new Set() - const onSearchFFTypeSet = new Set() - const itemsId = new Set() - let customMenuIds: any = [] - let customMenu = false - // Storing static fulfillment ids in onSearchFFIdsArray, OnSearchFFTypeSet - try { - logger.info(`Saving static fulfillment ids in /${constants.ON_SEARCH}`) - - onSearchCatalog['bpp/providers'].forEach((provider: any) => { - const onSearchFFIds = new Set() - const bppFF = provider.fulfillments - const len = bppFF.length - - let i = 0 - while (i < len) { - onSearchFFTypeSet.add(bppFF[i].type) - onSearchFFIds.add(bppFF[i].id) - i++ + if (!stateless && !_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + businessErrors[`Domain[${data.context.action}]`] = `Domain should be same in each action` } - onSearchFFIdsArray.push(onSearchFFIds) - }) - setValue('onSearchFFIdsArray', onSearchFFIdsArray) - } catch (error: any) { - logger.info(`Error while saving static fulfillment ids in /${constants.ON_SEARCH}, ${error.stack}`) - } + const checkBap = checkBppIdOrBapId(context.bap_id) + const checkBpp = checkBppIdOrBapId(context.bpp_id) - try { - logger.info(`Checking for upcoming holidays`) - const location = onSearchCatalog['bpp/providers'][0]['locations'] - if (!location) { - logger.error('No location detected ') - } + if (checkBap) Object.assign(businessErrors, { bap_id: 'context/bap_id should not be a url' }) + if (checkBpp) Object.assign(businessErrors, { bpp_id: 'context/bpp_id should not be a url' }) + const contextRes: any = checkContext(context, constants.ON_SEARCH) + if (!contextRes?.valid) { + Object.assign(businessErrors, contextRes.ERRORS) + } - const scheduleObject = location[0].time.schedule.holidays - const timestamp = context.timestamp - const [currentDate] = timestamp.split('T') + validateBapUri(context.bap_uri, context.bap_id, businessErrors) + validateBppUri(context.bpp_uri, context.bpp_id, businessErrors) - scheduleObject.map((date: string) => { - const dateObj = new Date(date) - const currentDateObj = new Date(currentDate) - if (dateObj.getTime() < currentDateObj.getTime()) { - const key = `/message/catalog/bpp/providers/loc${0}/time/schedule/holidays` - errorObj[key] = `Holidays cannot be past ${currentDate}` + if (context.transaction_id == context.message_id) { + businessErrors['on_search_full_catalog_refresh'] = + `Context transaction_id (${context.transaction_id}) and message_id (${context.message_id}) can't be the same.` } - }) - } catch (e) { - logger.error('No Holiday', e) - } - try { - logger.info(`Mapping items with thier respective providers`) - const itemProviderMap: any = {} - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - const items = provider.items - const itemArray: any = [] - items.forEach((item: any) => { - itemArray.push(item.id) - }) - itemProviderMap[provider.id] = itemArray - }) - - setValue('itemProviderMap', itemProviderMap) - } catch (e: any) { - logger.error(`Error while mapping items with thier respective providers ${e.stack}`) - } + setValue(`${ApiSequence.ON_SEARCH}`, data) - try { - logger.info(`Storing Item IDs in /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any, index: number) => { - const items = provider.items - items.forEach((item: any, j: number) => { - if (itemsId.has(item.id)) { - const key = `DuplicateItem[${j}]` - errorObj[key] = `duplicate item id: ${item.id} in bpp/providers[${index}]` - } else { - itemsId.add(item.id) - } - }) - }) - } catch (error: any) { - logger.error(`Error while storing Item IDs in /${constants.ON_SEARCH}, ${error.stack}`) - } + const searchContext: any = stateless ? null : getValue(`${ApiSequence.SEARCH}_context`) - try { - logger.info(`Checking for np_type in bpp/descriptor`) - const descriptor = onSearchCatalog['bpp/descriptor'] - descriptor?.tags.map((tag: { code: any; list: any[] }) => { - if (tag.code === 'bpp_terms') { - const npType = tag.list.find((item) => item.code === 'np_type') - if (!npType) { - errorObj['bpp/descriptor'] = `Missing np_type in bpp/descriptor` - setValue(`${ApiSequence.ON_SEARCH}np_type`, '') - } else { - setValue(`${ApiSequence.ON_SEARCH}np_type`, npType.value) - const npTypeValue = npType.value - if (npTypeValue !== 'ISN' && npTypeValue !== 'MSN') { - errorObj['bpp/descriptor/np_type'] = - `Invalid value '${npType.value}' for np_type. It should be either 'ISN' or 'MSN' in uppercase.` - } - } + try { + logger.info(`Storing BAP_ID and BPP_ID in /${constants.ON_SEARCH}`) + setValue('bapId', context.bap_id) + setValue('bppId', context.bpp_id) + } catch (error: any) { + logger.error(`!!Error while storing BAP and BPP Ids in /${constants.ON_SEARCH}, ${error.stack}`) + } - const accept_bap_terms = tag.list.find((item) => item.code === 'accept_bap_terms') - if (accept_bap_terms) { - errorObj['bpp/descriptor/accept_bap_terms'] = - `remove accept_bap_terms block in /bpp/descriptor/tags; should be enabled once BNP send their static terms in /search and are later accepted by SNP` - } + try { + logger.info(`Comparing timestamp of /${ApiSequence.SEARCH} /${constants.ON_SEARCH}`) - const collect_payment = tag.list.find((item) => item.code === 'collect_payment') - if (collect_payment) { - errorObj['bpp/descriptor/collect_payment'] = `collect_payment is not required in bpp/descriptor/tags ` + if (searchContext.timestamp == context.timestamp) { + businessErrors.tmstmp = `context/timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH} api cannot be same` } - } - }) - } catch (error: any) { - logger.error(`Error while checking np_type in bpp/descriptor for /${constants.ON_SEARCH}, ${error.stack}`) - } - - try { - logger.info(`Checking Providers info (bpp/providers) in /${constants.ON_SEARCH}`) - let i = 0 - const bppPrvdrs = onSearchCatalog['bpp/providers'] - const len = bppPrvdrs.length - const tmpstmp = context.timestamp - let itemIdList: any = [] - let itemsArray = [] - while (i < len) { - const categoriesId = new Set() - const customGrpId = new Set() - const seqSet = new Set() - const itemCategory_id = new Set() - const categoryRankSet = new Set() - const prvdrLocationIds = new Set() - - logger.info(`Validating uniqueness for provider id in bpp/providers[${i}]...`) - const prvdr = bppPrvdrs[i] - const categories = prvdr?.['categories'] - const items = prvdr?.['items'] - - if (prvdrsId.has(prvdr.id)) { - const key = `prvdr${i}id` - errorObj[key] = `duplicate provider id: ${prvdr.id} in bpp/providers` - } else { - prvdrsId.add(prvdr.id) + } catch (error: any) { + logger.error(`!!Error while Comparing timestamp of /${ApiSequence.SEARCH} /${constants.ON_SEARCH}, ${error.stack}`) } - logger.info(`Checking store enable/disable timestamp in bpp/providers[${i}]`) - const providerTime = new Date(prvdr.time.timestamp).getTime() - const contextTimestamp = new Date(tmpstmp).getTime() - setValue('tmpstmp', context.timestamp) - - if (providerTime > contextTimestamp) { - errorObj.StoreEnableDisable = `store enable/disable timestamp (/bpp/providers/time/timestamp) should be less then or equal to context.timestamp` + try { + logger.info(`Comparing transaction Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(searchContext.transaction_id, context.transaction_id)) { + businessErrors.transaction_id = `Transaction Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + } + } catch (error: any) { + logger.info( + `Error while comparing transaction ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + ) } - try { - const customGroupCategory = extractCustomGroups(categories) - - const baseTreeNodes = mapItemToTreeNode(items) + logger.info(`Checking customizations based on config.min and config.max values...`) - const customItems = mapCustomItemToTreeNode(items, customGroupCategory) + const items = getValue('items') // Retrieve items from the catalog or message - const mapItems = mapCustomizationsToBaseItems(baseTreeNodes, customItems) + _.filter(items, (item) => { + const customGroup = item.custom_group // Assuming custom_group holds the configuration data - const default_selection = calculateDefaultSelectionPrice(mapItems) + // Check for minimum customizations + if (customGroup?.config?.min === 1) { + logger.info(`Checking min value for item id: ${item.id}`) - default_selection.forEach(({ base_item, default_selection_calculated, default_selection_actual }) => { - if ( - !default_selection_calculated || - !default_selection_actual || - default_selection_calculated.min === undefined || - default_selection_actual.min === undefined || - default_selection_calculated.max === undefined || - default_selection_actual.max === undefined - ) { - const errorMessage = `Missing or invalid default_selection values for base_item ${base_item}. - Calculated: ${JSON.stringify(default_selection_calculated)}, - Actual: ${JSON.stringify(default_selection_actual)}.` + const defaultCustomizations = _.filter(item.customizations, (customization) => { + return customization.is_default // Check for default customizations + }) - logger.error(`Error in base_item ${base_item}: ${errorMessage}`) - errorObj[`providers[${i}][${base_item}]`] = errorMessage - return + if (defaultCustomizations.length < 1) { + const key = `item${item.id}CustomGroup/min` + businessErrors[key] = + `Item with id: ${item.id} must have at least one default customization as config.min is set to 1.` + } } - if ( - default_selection_calculated.min !== default_selection_actual.min || - default_selection_calculated.max !== default_selection_actual.max - ) { - const errorMessage = `Provided default_selection calculated incorrectly, - Calculated: min=${default_selection_calculated.min}, max=${default_selection_calculated.max}. - Given: min=${default_selection_actual.min}, max=${default_selection_actual.max}.` - logger.error(`Error in base_item ${base_item}: ${errorMessage}`) + // Check for maximum customizations + if (customGroup?.config?.max === 2) { + logger.info(`Checking max value for item id: ${item.id}`) - // Store error in errorObj with base_item as key - errorObj[`providers[${i}][${base_item}]`] = errorMessage - } else { - logger.info(`Base item ${base_item} values match. No error.`) + const customizationsCount = item.customizations.length + + if (customizationsCount > 2) { + const key = `item${item.id}CustomGroup/max` + businessErrors[key] = `Item with id: ${item.id} can have at most 2 customizations as config.max is set to 2.` + } } }) } catch (error: any) { - errorObj[`providers[${i}_default_selection`] = - `Error while Calculating Default Selection in /${constants.ON_SEARCH}, ${error.stack}` - logger.info(`Error while Calculating Default Selection in /${constants.ON_SEARCH}, ${error.stack}`) + logger.error(`Error while checking customizations for items, ${error.stack}`) } - // commented price range calculation - // try { - // // Parse raw data - // const parsedCategories = CatalogParser.parseCategories(categories) - // const menuItems = CatalogParser.parseMenuItems(items) - // const customizations = CatalogParser.parseCustomizations(items) - - // // Build menu trees with price calculations - // const treeBuilder = new MenuTreeBuilder(menuItems, parsedCategories, customizations) - // const itemsWithPricesAndLogs = treeBuilder.buildTrees() - - // itemsWithPricesAndLogs.items.forEach((item: any) => { - // console.log("itemOkay", item) - // const calculatedPriceRange = item.priceRange - // const givenPriceRange = item.priceRangeGiven - - // if ( - // calculatedPriceRange.lower !== givenPriceRange.lower || - // calculatedPriceRange.upper !== givenPriceRange.upper - // ) { - // const itemLog = CatalogParser.extractLogsForItem(itemsWithPricesAndLogs.logs, item.id) - - // errorObj[`providers[${i}][lower_upper_range-(${item.id})`] = - // `Provided price range calculated incorrectly for ${item.id}, - // Calculated: lower=${calculatedPriceRange.lower}, upper=${calculatedPriceRange.upper}. - // Given: lower=${givenPriceRange.lower}, upper=${givenPriceRange.upper}. - // Calculation: ${itemLog}` - // } - // }) - // } catch (error: any) { - // errorObj[`providers[${i}][lower_upper_range`] = - // `Error while Calculating Lower Upper Range in /${constants.ON_SEARCH}, ${error.stack}` - // logger.info(`Error while Calculating Lower Upper Range in /${constants.ON_SEARCH}, ${error.stack}`) - // } + + // removed timestamp difference check + // try { + // logger.info(`Comparing timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + // const tmpstmp = searchContext?.timestamp + // if (_.gte(tmpstmp, context.timestamp)) { + // businessErrors.tmpstmp = `Timestamp for /${constants.SEARCH} api cannot be greater than or equal to /${constants.ON_SEARCH} api` + // } else { + // const timeDiff = timeDifference(context.timestamp, tmpstmp) + // logger.info(timeDiff) + // if (timeDiff > 5000) { + // businessErrors.tmpstmp = `context/timestamp difference between /${constants.ON_SEARCH} and /${constants.SEARCH} should be less than 5 sec` + // } + // } + // } catch (error: any) { + // logger.info( + // `Error while comparing timestamp for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + // ) + // } try { - logger.info(`Checking length of strings provided in descriptor /${constants.ON_SEARCH}`) - const descriptor = prvdr['descriptor'] - const result = validateObjectString(descriptor) - if (typeof result == 'string' && result.length) { - const key = `prvdr${i}descriptor` - errorObj[key] = result + logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(searchContext.message_id, context.message_id)) { + businessErrors.message_id = `Message Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` } } catch (error: any) { logger.info( - `Error while Checking length of strings provided in descriptor /${constants.ON_SEARCH}, ${error.stack}`, + `Error while comparing message ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, ) } + const onSearchCatalog: any = message.catalog + const onSearchFFIdsArray: any = [] + const prvdrsId = new Set() + const prvdrLocId = new Set() + const onSearchFFTypeSet = new Set() + const itemsId = new Set() + let customMenuIds: any = [] + let customMenu = false + // Storing static fulfillment ids in onSearchFFIdsArray, OnSearchFFTypeSet try { - logger.info(`Checking for empty list arrays in tags`) - const categories = prvdr['categories'] - categories.forEach( - (category: { id: string; parent_category_id: string; descriptor: { name: string }; tags: any[] }) => { - if (category.parent_category_id === category.id) { - errorObj[`categories[${category.id}].prnt_ctgry_id`] = - `/message/catalog/bpp/providers/categories/parent_category_id should not be the same as id in category '${category.descriptor.name}'` - } - category.tags.forEach((tag: { code: string; list: any[] }, index: number) => { - if (tag.list.length === 0) { - errorObj[`provider[${i}].categories[${category.id}].tags[${index}]`] = - `Empty list array provided for tag '${tag.code}' in category '${category.descriptor.name}'` - } - if (tag.code === 'display') { - tag.list.forEach((item: { code: string; value: string }) => { - if (item.code === 'rank' && parseInt(item.value) === 0) { - errorObj[`provider[${i}].categories[${category.id}].tags[${index}].list[${item.code}]`] = - `display rank provided in /message/catalog/bpp/providers/categories (category:'${category?.descriptor?.name}) should not be zero ("0"), it should start from one ('1') '` - } - }) - } - if (tag.code === 'config') { - tag.list.forEach((item: { code: string; value: string }) => { - if (item.code === 'seq' && parseInt(item.value) === 0) { - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `Seq value should start from 1 and not 0 in category '${category.descriptor.name}'` - } - }) - } - if (tag.code === 'type') { - tag.list.forEach((item: { code: string; value: string }) => { - if (item.code === 'type') { - if ( - (category.parent_category_id == '' || category.parent_category_id) && - item.value == 'custom_group' - ) { - if (category.parent_category_id) { - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `parent_category_id should not have any value while type is ${item.value}` - } - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `parent_category_id should not be present while type is ${item.value}` - } else if ( - category.parent_category_id != '' && - (item.value == 'custom_menu' || item.value == 'variant_group') - ) { - if (category.parent_category_id) { - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `parent_category_id should be empty string while type is ${item.value}` - } - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `parent_category_id should be present while type is ${item.value}` - } else if ( - category.parent_category_id && - (item.value == 'custom_menu' || item.value == 'variant_group') - ) { - if (category.parent_category_id) { - errorObj[`categories[${category.id}].tags[${index}].list[${item.code}]`] = - `parent_category_id should be empty string while type is ${item.value}` - } - } - } - }) - } - }) - }, - ) + logger.info(`Saving static fulfillment ids in /${constants.ON_SEARCH}`) + + onSearchCatalog['bpp/providers'].forEach((provider: any) => { + const onSearchFFIds = new Set() + const bppFF = provider.fulfillments + const len = bppFF.length + + let i = 0 + while (i < len) { + onSearchFFTypeSet.add(bppFF[i].type) + onSearchFFIds.add(bppFF[i].id) + i++ + } + onSearchFFIdsArray.push(onSearchFFIds) + }) + + setValue('onSearchFFIdsArray', onSearchFFIdsArray) } catch (error: any) { - logger.error(`Error while checking empty list arrays in tags for /${constants.ON_SEARCH}, ${error.stack}`) + logger.info(`Error while saving static fulfillment ids in /${constants.ON_SEARCH}, ${error.stack}`) } try { - // Adding items in a list - const items = prvdr.items - itemsArray.push(items) - items.forEach((item: any) => { - itemIdList.push(item.id) + logger.info(`Checking for upcoming holidays`) + const location = onSearchCatalog['bpp/providers'][0]['locations'] + if (!location) { + logger.error('No location detected ') + } + + const scheduleObject = location[0].time.schedule.holidays + const timestamp = context.timestamp + const [currentDate] = timestamp.split('T') + + scheduleObject.map((date: string) => { + const dateObj = new Date(date) + const currentDateObj = new Date(currentDate) + if (dateObj.getTime() < currentDateObj.getTime()) { + const key = `/message/catalog/bpp/providers/loc${0}/time/schedule/holidays` + businessErrors[key] = `Holidays cannot be past ${currentDate}` + } }) - setValue('ItemList', itemIdList) - } catch (error: any) { - logger.error(`Error while adding items in a list, ${error.stack}`) + } catch (e) { + logger.error('No Holiday', e) } try { - try { - if (flow === FLOW.FLOW00A) { - bppPrvdrs.forEach((provider: any, i: number) => { - provider.items.forEach(async (item: any, j: number) => { - const npFeesTag = await validateNpFees(item, provider.categories, provider, flow, errorObj, i, j) - console.log("npFeesTag",npFeesTag); - - - if (npFeesTag) { - // do something with npFeesTag.list - } + logger.info(`Mapping items with thier respective providers`) + const itemProviderMap: any = {} + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + const items = provider.items + const itemArray: any = [] + items.forEach((item: any) => { + itemArray.push(item.id) }) + itemProviderMap[provider.id] = itemArray }) - } - } catch (error) { - - } - } catch (error) { - - } - logger.info(`Checking store timings in bpp/providers[${i}]`) + setValue('itemProviderMap', itemProviderMap) + } catch (e: any) { + logger.error(`Error while mapping items with thier respective providers ${e.stack}`) + } - prvdr.locations.forEach((loc: any, iter: any) => { - try { - logger.info(`Checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`) - const has = Object.prototype.hasOwnProperty - if (has.call(loc, 'gps')) { - if (!checkGpsPrecision(loc.gps)) { - errorObj.gpsPrecision = `/bpp/providers[${i}]/locations[${iter}]/gps coordinates must be specified with at least 4 decimal places of precision.` + try { + logger.info(`Storing Item IDs in /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any, index: number) => { + const items = provider.items + items.forEach((item: any, j: number) => { + if (itemsId.has(item.id)) { + const key = `DuplicateItem[${j}]` + businessErrors[key] = `duplicate item id: ${item.id} in bpp/providers[${index}]` + } else { + itemsId.add(item.id) } - } - } catch (error) { - logger.error( - `!!Error while checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`, - error, - ) - } - - if (prvdrLocId.has(loc.id)) { - const key = `prvdr${i}${loc.id}${iter}` - errorObj[key] = `duplicate location id: ${loc.id} in /bpp/providers[${i}]/locations[${iter}]` - } else { - prvdrLocId.add(loc.id) - } - prvdrLocationIds.add(loc?.id) - logger.info('Checking store days...') - const days = loc.time.days.split(',') - days.forEach((day: any) => { - day = parseInt(day) - if (isNaN(day) || day < 1 || day > 7) { - const key = `prvdr${i}locdays${iter}` - errorObj[key] = - `store days (bpp/providers[${i}]/locations[${iter}]/time/days) should be in the format ("1,2,3,4,5,6,7") where 1- Monday and 7- Sunday` - } + }) }) + } catch (error: any) { + logger.error(`Error while storing Item IDs in /${constants.ON_SEARCH}, ${error.stack}`) + } - logger.info('Checking fixed or split timings') - //scenario 1: range =1 freq/times =1 - if (loc.time.range && (loc.time.schedule?.frequency || loc.time.schedule?.times)) { - const key = `prvdr${i}loctime${iter}` - errorObj[key] = - `Either one of fixed (range) or split (frequency and times) timings should be provided in /bpp/providers[${i}]/locations[${iter}]/time` - } + try { + logger.info(`Checking for np_type in bpp/descriptor`) + const descriptor = onSearchCatalog['bpp/descriptor'] + descriptor?.tags.map((tag: { code: any; list: any[] }) => { + if (tag.code === 'bpp_terms') { + const npType = tag.list.find((item) => item.code === 'np_type') + if (!npType) { + businessErrors['bpp/descriptor'] = `Missing np_type in bpp/descriptor` + setValue(`${ApiSequence.ON_SEARCH}np_type`, '') + } else { + setValue(`${ApiSequence.ON_SEARCH}np_type`, npType.value) + const npTypeValue = npType.value + if (npTypeValue !== 'ISN' && npTypeValue !== 'MSN') { + businessErrors['bpp/descriptor/np_type'] = + `Invalid value '${npType.value}' for np_type. It should be either 'ISN' or 'MSN' in uppercase.` + } + } - // scenario 2: range=0 freq || times =1 - if (!loc.time.range && (!loc.time.schedule.frequency || !loc.time.schedule.times)) { - const key = `prvdr${i}loctime${iter}` - errorObj[key] = - `Either one of fixed timings (range) or split timings (both frequency and times) should be provided in /bpp/providers[${i}]/locations[${iter}]/time` - } + const accept_bap_terms = tag.list.find((item) => item.code === 'accept_bap_terms') + if (accept_bap_terms) { + businessErrors['bpp/descriptor/accept_bap_terms'] = + `remove accept_bap_terms block in /bpp/descriptor/tags; should be enabled once BNP send their static terms in /search and are later accepted by SNP` + } - //scenario 3: range=1 (start and end not compliant) frequency=0; - if ('range' in loc.time) { - logger.info('checking range (fixed timings) start and end') - const startTime: any = 'start' in loc.time.range ? parseInt(loc.time.range.start) : '' - const endTime: any = 'end' in loc.time.range ? parseInt(loc.time.range.end) : '' - if (isNaN(startTime) || isNaN(endTime) || startTime > endTime || endTime > 2359) { - errorObj.startEndTime = `end time must be greater than start time in fixed timings /locations/time/range (fixed store timings)` + const collect_payment = tag.list.find((item) => item.code === 'collect_payment') + if (collect_payment) { + businessErrors['bpp/descriptor/collect_payment'] = `collect_payment is not required in bpp/descriptor/tags ` + } } - } - }) - - try { - // Adding items in a list - const items = prvdr.items - items.forEach((item: any) => { - itemIdList.push(item.id) }) - setValue('ItemList', itemIdList) } catch (error: any) { - logger.error(`Error while adding items in a list, ${error.stack}`) + logger.error(`Error while checking np_type in bpp/descriptor for /${constants.ON_SEARCH}, ${error.stack}`) } try { - logger.info(`Checking categories for provider (${prvdr.id}) in bpp/providers[${i}]`) - let j = 0 - const categories = onSearchCatalog['bpp/providers'][i]['categories'] - if (!categories || !categories.length) { - const key = `prvdr${i}categories` - errorObj[key] = `Support for variants is mandatory, categories must be present in bpp/providers[${i}]` - } - const iLen = categories.length - while (j < iLen) { - logger.info(`Validating uniqueness for categories id in bpp/providers[${i}].items[${j}]...`) - const category = categories[j] - - const fulfillments = onSearchCatalog['bpp/providers'][i]['fulfillments'] - const phoneNumber = fulfillments[i].contact.phone - - if (!isValidPhoneNumber(phoneNumber)) { - const key = `bpp/providers${i}fulfillments${i}` - errorObj[key] = - `Please enter a valid phone number consisting of 10 or 11 digits without any spaces or special characters. ` + logger.info(`Checking Providers info (bpp/providers) in /${constants.ON_SEARCH}`) + let i = 0 + const bppPrvdrs = onSearchCatalog['bpp/providers'] + const len = bppPrvdrs.length + const tmpstmp = context.timestamp + let itemIdList: any = [] + let itemsArray = [] + while (i < len) { + const categoriesId = new Set() + const customGrpId = new Set() + const seqSet = new Set() + const itemCategory_id = new Set() + const categoryRankSet = new Set() + const prvdrLocationIds = new Set() + + logger.info(`Validating uniqueness for provider id in bpp/providers[${i}]...`) + const prvdr = bppPrvdrs[i] + + //Validating media in bpp/provider[r].descriptor + try { + logger.info(`Validating media array in bpp/providers[${i}].descriptor`) + const descriptor = prvdr.descriptor + // Only validate if media exists and is an array (optional field) + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype is required and must be a string in media[${mediaIdx}]` + } + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url is required and must be a string in media[${mediaIdx}]` + } + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` + } + }) + } + } catch (error: any) { + logger.error(`Error while validating media array in bpp/providers[${i}].descriptor: ${error.stack}`) } - if (categoriesId.has(category.id)) { - const key = `prvdr${i}category${j}` - errorObj[key] = `duplicate category id: ${category.id} in bpp/providers[${i}]` + const categories = prvdr?.['categories'] + const items = prvdr?.['items'] + + if (prvdrsId.has(prvdr.id)) { + const key = `prvdr${i}id` + businessErrors[key] = `duplicate provider id: ${prvdr.id} in bpp/providers` } else { - categoriesId.add(category.id) + prvdrsId.add(prvdr.id) } - try { - category.tags.map((tag: { code: any; list: any[] }, index: number) => { - switch (tag.code) { - case 'type': - const codeList = tag.list.find((item) => item.code === 'type') - if ( - !( - codeList.value === 'custom_menu' || - codeList.value === 'custom_group' || - codeList.value === 'variant_group' - ) - ) { - const key = `prvdr${i}category${j}tags${index}` - errorObj[key] = - `list.code == type then value should be one of 'custom_menu','custom_group' and 'variant_group' in bpp/providers[${i}]` - } + logger.info(`Checking store enable/disable timestamp in bpp/providers[${i}]`) + const providerTime = new Date(prvdr.time.timestamp).getTime() + const contextTimestamp = new Date(tmpstmp).getTime() + setValue('tmpstmp', context.timestamp) - if (codeList.value === 'custom_group') { - customGrpId.add(category.id) - } + if (providerTime > contextTimestamp) { + errorObj.StoreEnableDisable = `store enable/disable timestamp (/bpp/providers/time/timestamp) should be less then or equal to context.timestamp` + } - break - case 'timing': - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj.custom_menu_timing_tag = `Invalid value for '${item.code}': ${item.value}` - } + try { + const customGroupCategory = extractCustomGroups(categories) - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - errorObj.time_to = `Invalid time format for '${item.code}': ${item.value}` - } + const baseTreeNodes = mapItemToTreeNode(items) - break - default: - errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` - } - } + const customItems = mapCustomItemToTreeNode(items, customGroupCategory) - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + const mapItems = mapCustomizationsToBaseItems(baseTreeNodes, customItems) - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) + const default_selection = calculateDefaultSelectionPrice(mapItems) - if (dayTo < dayFrom) { - errorObj.day_from = "'day_to' must be greater than or equal to 'day_from'" - } + default_selection.forEach(({ base_item, default_selection_calculated, default_selection_actual }) => { + if ( + !default_selection_calculated || + !default_selection_actual || + default_selection_calculated.min === undefined || + default_selection_actual.min === undefined || + default_selection_calculated.max === undefined || + default_selection_actual.max === undefined + ) { + const errorMessage = `Missing or invalid default_selection values for base_item ${base_item}. + Calculated: ${JSON.stringify(default_selection_calculated)}, + Actual: ${JSON.stringify(default_selection_actual)}.` - if (timeTo <= timeFrom) { - errorObj.time_from = "'time_to' must be greater than 'time_from'" - } - } + logger.error(`Error in base_item ${base_item}: ${errorMessage}`) + businessErrors[`providers[${i}][${base_item}]`] = errorMessage + return + } + if ( + default_selection_calculated.min !== default_selection_actual.min || + default_selection_calculated.max !== default_selection_actual.max + ) { + const errorMessage = `Provided default_selection calculated incorrectly, + Calculated: min=${default_selection_calculated.min}, max=${default_selection_calculated.max}. + Given: min=${default_selection_actual.min}, max=${default_selection_actual.max}.` - break - case 'display': - for (const item of tag.list) { - if (item.code !== 'rank' || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj.rank = `Invalid value for 'display': ${item.value}` - } else { - if (categoryRankSet.has(category.id)) { - const key = `prvdr${i}category${j}rank` - errorObj[key] = `duplicate rank in category id: ${category.id} in bpp/providers[${i}]` - } else { - categoryRankSet.add(category.id) - } - } - } + logger.error(`Error in base_item ${base_item}: ${errorMessage}`) - break - case 'config': - const minItem: any = tag.list.find((item: { code: string }) => item.code === 'min') - const maxItem: any = tag.list.find((item: { code: string }) => item.code === 'max') - const inputItem: any = tag.list.find((item: { code: string }) => item.code === 'input') - const seqItem: any = tag.list.find((item: { code: string }) => item.code === 'seq') + // Store error in errorObj with base_item as key + businessErrors[`providers[${i}][${base_item}]`] = errorMessage + } else { + logger.info(`Base item ${base_item} values match. No error.`) + } + }) + } catch (error: any) { + businessErrors[`providers[${i}_default_selection`] = + `Error while Calculating Default Selection in /${constants.ON_SEARCH}, ${error.stack}` + logger.info(`Error while Calculating Default Selection in /${constants.ON_SEARCH}, ${error.stack}`) + } + // commented price range calculation + // try { + // // Parse raw data + // const parsedCategories = CatalogParser.parseCategories(categories) + // const menuItems = CatalogParser.parseMenuItems(items) + // const customizations = CatalogParser.parseCustomizations(items) + + // // Build menu trees with price calculations + // const treeBuilder = new MenuTreeBuilder(menuItems, parsedCategories, customizations) + // const itemsWithPricesAndLogs = treeBuilder.buildTrees() + + // itemsWithPricesAndLogs.items.forEach((item: any) => { + // console.log("itemOkay", item) + // const calculatedPriceRange = item.priceRange + // const givenPriceRange = item.priceRangeGiven + + // if ( + // calculatedPriceRange.lower !== givenPriceRange.lower || + // calculatedPriceRange.upper !== givenPriceRange.upper + // ) { + // const itemLog = CatalogParser.extractLogsForItem(itemsWithPricesAndLogs.logs, item.id) + + // businessErrors[`providers[${i}][lower_upper_range-(${item.id})`] = + // `Provided price range calculated incorrectly for ${item.id}, + // Calculated: lower=${calculatedPriceRange.lower}, upper=${calculatedPriceRange.upper}. + // Given: lower=${givenPriceRange.lower}, upper=${givenPriceRange.upper}. + // Calculation: ${itemLog}` + // } + // }) + // } catch (error: any) { + // businessErrors[`providers[${i}][lower_upper_range`] = + // `Error while Calculating Lower Upper Range in /${constants.ON_SEARCH}, ${error.stack}` + // logger.info(`Error while Calculating Lower Upper Range in /${constants.ON_SEARCH}, ${error.stack}`) + // } - if (!minItem || !maxItem) { - errorObj[`customization_config_${j}`] = - `Both 'min' and 'max' values are required in 'config' at index: ${j}` - } + try { + logger.info(`Checking length of strings provided in descriptor /${constants.ON_SEARCH}`) + const descriptor = prvdr['descriptor'] + const result = validateObjectString(descriptor) + if (typeof result == 'string' && result.length) { + const key = `prvdr${i}descriptor` + businessErrors[key] = result + } + } catch (error: any) { + logger.info( + `Error while Checking length of strings provided in descriptor /${constants.ON_SEARCH}, ${error.stack}`, + ) + } - if (!/^-?\d+(\.\d+)?$/.test(minItem.value)) { - errorObj[`customization_config_min_${j}`] = - `Invalid value for ${minItem.code}: ${minItem.value} at index: ${j}` + try { + logger.info(`Checking for empty list arrays in tags`) + const categories = prvdr['categories'] + categories.forEach( + (category: { id: string; parent_category_id: string; descriptor: { name: string }; tags: any[] }) => { + if (category.parent_category_id === category.id) { + businessErrors[`categories[${category.id}].prnt_ctgry_id`] = + `/message/catalog/bpp/providers/categories/parent_category_id should not be the same as id in category '${category.descriptor.name}'` + } + category.tags.forEach((tag: { code: string; list: any[] }, index: number) => { + if (tag.list.length === 0) { + businessErrors[`provider[${i}].categories[${category.id}].tags[${index}]`] = + `Empty list array provided for tag '${tag.code}' in category '${category.descriptor.name}'` } - - if (!/^-?\d+(\.\d+)?$/.test(maxItem.value)) { - errorObj[`customization_config_max_${j}`] = - `Invalid value for ${maxItem.code}: ${maxItem.value}at index: ${j}` + if (tag.code === 'display') { + tag.list.forEach((item: { code: string; value: string }) => { + if (item.code === 'rank' && parseInt(item.value) === 0) { + businessErrors[`provider[${i}].categories[${category.id}].tags[${index}].list[${item.code}]`] = + `display rank provided in /message/catalog/bpp/providers/categories (category:'${category?.descriptor?.name}) should not be zero ("0"), it should start from one ('1') '` + } + }) } - - if (!/^-?\d+(\.\d+)?$/.test(seqItem.value)) { - errorObj[`config_seq_${j}`] = `Invalid value for ${seqItem.code}: ${seqItem.value} at index: ${j}` + if (tag.code === 'config') { + tag.list.forEach((item: { code: string; value: string }) => { + if (item.code === 'seq' && parseInt(item.value) === 0) { + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `Seq value should start from 1 and not 0 in category '${category.descriptor.name}'` + } + }) } - - const inputEnum = ['select', 'text'] - if (!inputEnum.includes(inputItem.value)) { - errorObj[`config_input_${j}`] = - `Invalid value for 'input': ${inputItem.value}, it should be one of ${inputEnum} at index: ${j}` + if (tag.code === 'type') { + tag.list.forEach((item: { code: string; value: string }) => { + if (item.code === 'type') { + if ( + (category.parent_category_id == '' || category.parent_category_id) && + item.value == 'custom_group' + ) { + if (category.parent_category_id) { + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `parent_category_id should not have any value while type is ${item.value}` + } + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `parent_category_id should not be present while type is ${item.value}` + } else if ( + category.parent_category_id != '' && + (item.value == 'custom_menu' || item.value == 'variant_group') + ) { + if (category.parent_category_id) { + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `parent_category_id should be empty string while type is ${item.value}` + } + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `parent_category_id should be present while type is ${item.value}` + } else if ( + category.parent_category_id && + (item.value == 'custom_menu' || item.value == 'variant_group') + ) { + if (category.parent_category_id) { + businessErrors[`categories[${category.id}].tags[${index}].list[${item.code}]`] = + `parent_category_id should be empty string while type is ${item.value}` + } + } + } + }) } - - break - } - }) - logger.info(`Category '${category.descriptor.name}' is valid.`) + }) + }, + ) } catch (error: any) { - logger.error(`Validation error for category '${category.descriptor.name}': ${error.message}`) - } - - j++ - } - } catch (error: any) { - logger.error(`!!Errors while checking categories in bpp/providers[${i}], ${error.stack}`) - } - - try { - logger.info(`Checking items for provider (${prvdr.id}) in bpp/providers[${i}]`) - let j = 0 - const items = onSearchCatalog['bpp/providers'][i]['items'] - const iLen = items.length - while (j < iLen) { - logger.info(`Validating uniqueness for item id in bpp/providers[${i}].items[${j}]...`) - const item = items[j] - - if ('category_id' in item) { - itemCategory_id.add(item.category_id) + logger.error(`Error while checking empty list arrays in tags for /${constants.ON_SEARCH}, ${error.stack}`) } - if ('category_ids' in item) { - item[`category_ids`].map((category: string, index: number) => { - const categoryId = category.split(':')[0] - const seq = category.split(':')[1] - - // Check if seq exists in category_ids - const seqExists = item[`category_ids`].some((cat: any) => cat.seq === seq) - - if (seqExists) { - const key = `prvdr${i}item${j}ctgryseq${index}` - errorObj[key] = `duplicate seq : ${seq} in category_ids in prvdr${i}item${j}` - } else { - seqSet.add(seq) - } - - if (!categoriesId.has(categoryId)) { - const key = `prvdr${i}item${j}ctgryId${index}` - errorObj[key] = `item${j} should have category_ids one of the Catalog/categories/id` - } + try { + // Adding items in a list + const items = prvdr.items + itemsArray.push(items) + items.forEach((item: any) => { + itemIdList.push(item.id) }) + setValue('ItemList', itemIdList) + } catch (error: any) { + logger.error(`Error while adding items in a list, ${error.stack}`) } - let lower_and_upper_not_present: boolean = true - let default_selection_not_present: boolean = true try { - logger.info(`Checking selling price and maximum price for item id: ${item.id}`) - - if ('price' in item) { - const sPrice = parseFloat(item.price.value) - const maxPrice = parseFloat(item.price.maximum_value) + try { + if (flow === FLOW.FLOW00A) { + bppPrvdrs.forEach((provider: any, i: number) => { + provider.items.forEach(async (item: any, j: number) => { + const npFeesTag = await validateNpFees(item, provider.categories, provider, flow, errorObj, i, j) + console.log("npFeesTag", npFeesTag); - const lower = parseFloat(item.price?.tags?.[0].list[0]?.value) - const upper = parseFloat(item.price?.tags?.[0].list[1]?.value) - if (lower >= 0 && upper >= 0) { - lower_and_upper_not_present = false + if (npFeesTag) { + // do something with npFeesTag.list + } + }) + }) } + } catch (error) { - const default_selection_value = parseFloat(item.price?.tags?.[1].list[0]?.value) - const default_selection_max_value = parseFloat(item.price?.tags?.[1].list[1]?.value) - - if (default_selection_value >= 0 && default_selection_max_value >= 0) { - default_selection_not_present = false - } + } + } catch (error) { - if (sPrice > maxPrice) { - const key = `prvdr${i}item${j}Price` - errorObj[key] = - `selling price of item /price/value with id: (${item.id}) can't be greater than the maximum price /price/maximum_value in /bpp/providers[${i}]/items[${j}]/` - } + } - if (upper < lower) { - const key = `prvdr${i}item${j}price/tags/` - errorObj[key] = - `selling lower range: ${lower} of code: range with id: (${item.id}) can't be greater than the upper range : ${upper} ` - } + logger.info(`Checking store timings in bpp/providers[${i}]`) - if (default_selection_max_value < default_selection_value) { - const key = `prvdr${i}item${j}Price/tags` - errorObj[key] = - `value : ${default_selection_value} of code: default_selection with id: (${item.id}) can't be greater than the maximum_value : ${default_selection_max_value} ` + prvdr.locations.forEach((loc: any, iter: any) => { + try { + logger.info(`Checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`) + const has = Object.prototype.hasOwnProperty + if (has.call(loc, 'gps')) { + if (!checkGpsPrecision(loc.gps)) { + errorObj.gpsPrecision = `/bpp/providers[${i}]/locations[${iter}]/gps coordinates must be specified with at least 4 decimal places of precision.` + } } + } catch (error) { + logger.error( + `!!Error while checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`, + error, + ) } - } catch (e: any) { - logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${e.stack}`) - } - try { - logger.info(`Checking fulfillment_id for item id: ${item.id}`) - if (item.fulfillment_id && !onSearchFFIdsArray[i].has(item.fulfillment_id)) { - const key = `prvdr${i}item${j}ff` - errorObj[key] = - `fulfillment_id in /bpp/providers[${i}]/items[${j}] should map to one of the fulfillments id in bpp/prvdr${i}/fulfillments ` + if (prvdrLocId.has(loc.id)) { + const key = `prvdr${i}${loc.id}${iter}` + businessErrors[key] = `duplicate location id: ${loc.id} in /bpp/providers[${i}]/locations[${iter}]` + } else { + prvdrLocId.add(loc.id) + } + prvdrLocationIds.add(loc?.id) + logger.info('Checking store days...') + if (loc.time && typeof loc.time.days === 'string') { + const days = loc.time.days.split(',') + days.forEach((day: any) => { + day = parseInt(day) + if (isNaN(day) || day < 1 || day > 7) { + const key = `prvdr${i}locdays${iter}` + businessErrors[key] = + `store days (bpp/providers[${i}]/locations[${iter}]/time/days) should be in the format ("1,2,3,4,5,6,7") where 1- Monday and 7- Sunday` + } + }) + } else { + logger.error(`loc.time.days is missing or not a string for bpp/providers[${i}]/locations[${iter}]`) } - } catch (e: any) { - logger.error(`Error while checking fulfillment_id for item id: ${item.id}, ${e.stack}`) - } - try { - logger.info(`Checking location_id for item id: ${item.id}`) + logger.info('Checking fixed or split timings') + //scenario 1: range =1 freq/times =1 + if (loc.time.range && (loc.time.schedule?.frequency || loc.time.schedule?.times)) { + const key = `prvdr${i}loctime${iter}` + businessErrors[key] = + `Either one of fixed (range) or split (frequency and times) timings should be provided in /bpp/providers[${i}]/locations[${iter}]/time` + } + + // scenario 2: range=0 freq || times =1 + if (!loc.time.range && (!loc.time.schedule.frequency || !loc.time.schedule.times)) { + const key = `prvdr${i}loctime${iter}` + businessErrors[key] = + `Either one of fixed timings (range) or split timings (both frequency and times) should be provided in /bpp/providers[${i}]/locations[${iter}]/time` + } - if (item.location_id && !prvdrLocId.has(item.location_id)) { - const key = `prvdr${i}item${j}loc` - errorObj[key] = - `location_id in /bpp/providers[${i}]/items[${j}] should be one of the locations id in /bpp/providers[${i}]/locations` + //scenario 3: range=1 (start and end not compliant) frequency=0; + if ('range' in loc.time) { + logger.info('checking range (fixed timings) start and end') + const startTime: any = 'start' in loc.time.range ? parseInt(loc.time.range.start) : '' + const endTime: any = 'end' in loc.time.range ? parseInt(loc.time.range.end) : '' + if (isNaN(startTime) || isNaN(endTime) || startTime > endTime || endTime > 2359) { + errorObj.startEndTime = `end time must be greater than start time in fixed timings /locations/time/range (fixed store timings)` + } } - } catch (e: any) { - logger.error(`Error while checking location_id for item id: ${item.id}, ${e.stack}`) + }) + + try { + // Adding items in a list + const items = prvdr.items + items.forEach((item: any) => { + itemIdList.push(item.id) + }) + setValue('ItemList', itemIdList) + } catch (error: any) { + logger.error(`Error while adding items in a list, ${error.stack}`) } try { - logger.info(`Checking consumer care details for item id: ${item.id}`) - if ('@ondc/org/contact_details_consumer_care' in item) { - let consCare = item['@ondc/org/contact_details_consumer_care'] - consCare = consCare.split(',') - if (consCare.length < 3) { - const key = `prvdr${i}consCare` - errorObj[key] = - `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` + logger.info(`Checking categories for provider (${prvdr.id}) in bpp/providers[${i}]`) + let j = 0 + const categories = onSearchCatalog['bpp/providers'][i]['categories'] + if (!categories || !categories.length) { + const key = `prvdr${i}categories` + businessErrors[key] = `Support for variants is mandatory, categories must be present in bpp/providers[${i}]` + } + const iLen = categories.length + while (j < iLen) { + logger.info(`Validating uniqueness for categories id in bpp/providers[${i}].items[${j}]...`) + const category = categories[j] + + const fulfillments = onSearchCatalog['bpp/providers'][i]['fulfillments'] + if (fulfillments && fulfillments.length > 0) { + fulfillments.forEach((fulfillment: any, fulfillmentIndex: number) => { + const phoneNumber = fulfillment?.contact?.phone + if (phoneNumber && !isValidPhoneNumber(phoneNumber)) { + const key = `bpp/providers${i}fulfillments${fulfillmentIndex}` + businessErrors[key] = + `Please enter a valid phone number consisting of 10 or 11 digits without any spaces or special characters.` + } + }) + } + // const phoneNumber = fulfillments[i].contact.phone + + // if (!isValidPhoneNumber(phoneNumber)) { + // const key = `bpp/providers${i}fulfillments${i}` + // businessErrors[key] = + // `Please enter a valid phone number consisting of 10 or 11 digits without any spaces or special characters. ` + // } + + if (categoriesId.has(category.id)) { + const key = `prvdr${i}category${j}` + businessErrors[key] = `duplicate category id: ${category.id} in bpp/providers[${i}]` } else { - const checkEmail: boolean = emailRegex(consCare[1].trim()) - if (isNaN(consCare[2].trim()) || !checkEmail) { - const key = `prvdr${i}consCare` - errorObj[key] = - `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` - } + categoriesId.add(category.id) } + + try { + category.tags.map((tag: { code: any; list: any[] }, index: number) => { + switch (tag.code) { + case 'type': + const codeList = tag.list.find((item) => item.code === 'type') + if ( + !( + codeList.value === 'custom_menu' || + codeList.value === 'custom_group' || + codeList.value === 'variant_group' + ) + ) { + const key = `prvdr${i}category${j}tags${index}` + businessErrors[key] = + `list.code == type then value should be one of 'custom_menu','custom_group' and 'variant_group' in bpp/providers[${i}]` + } + + if (codeList.value === 'custom_group') { + customGrpId.add(category.id) + } + + break + case 'timing': + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + errorObj.custom_menu_timing_tag = `Invalid value for '${item.code}': ${item.value}` + } + + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + errorObj.time_to = `Invalid time format for '${item.code}': ${item.value}` + } + + break + default: + errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` + } + } + + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) + + if (dayTo < dayFrom) { + errorObj.day_from = "'day_to' must be greater than or equal to 'day_from'" + } + + if (timeTo <= timeFrom) { + errorObj.time_from = "'time_to' must be greater than 'time_from'" + } + } + + break + case 'display': + for (const item of tag.list) { + if (item.code !== 'rank' || !/^-?\d+(\.\d+)?$/.test(item.value)) { + errorObj.rank = `Invalid value for 'display': ${item.value}` + } else { + if (categoryRankSet.has(category.id)) { + const key = `prvdr${i}category${j}rank` + businessErrors[key] = `duplicate rank in category id: ${category.id} in bpp/providers[${i}]` + } else { + categoryRankSet.add(category.id) + } + } + } + + break + case 'config': + const minItem: any = tag.list.find((item: { code: string }) => item.code === 'min') + const maxItem: any = tag.list.find((item: { code: string }) => item.code === 'max') + const inputItem: any = tag.list.find((item: { code: string }) => item.code === 'input') + const seqItem: any = tag.list.find((item: { code: string }) => item.code === 'seq') + + if (!minItem || !maxItem) { + businessErrors[`customization_config_${j}`] = + `Both 'min' and 'max' values are required in 'config' at index: ${j}` + } + + if (!/^-?\d+(\.\d+)?$/.test(minItem.value)) { + businessErrors[`customization_config_min_${j}`] = + `Invalid value for ${minItem.code}: ${minItem.value} at index: ${j}` + } + + if (!/^-?\d+(\.\d+)?$/.test(maxItem.value)) { + businessErrors[`customization_config_max_${j}`] = + `Invalid value for ${maxItem.code}: ${maxItem.value}at index: ${j}` + } + + if (!/^-?\d+(\.\d+)?$/.test(seqItem.value)) { + businessErrors[`config_seq_${j}`] = `Invalid value for ${seqItem.code}: ${seqItem.value} at index: ${j}` + } + + const inputEnum = ['select', 'text'] + if (!inputEnum.includes(inputItem.value)) { + businessErrors[`config_input_${j}`] = + `Invalid value for 'input': ${inputItem.value}, it should be one of ${inputEnum} at index: ${j}` + } + + break + } + }) + logger.info(`Category '${category.descriptor.name}' is valid.`) + } catch (error: any) { + logger.error(`Validation error for category '${category.descriptor.name}': ${error.message}`) + } + + j++ } - } catch (e: any) { - logger.error(`Error while checking consumer care details for item id: ${item.id}, ${e.stack}`) + } catch (error: any) { + logger.error(`!!Errors while checking categories in bpp/providers[${i}], ${error.stack}`) } try { - item.tags.map((tag: { code: any; list: any[] }, index: number) => { - switch (tag.code) { - case 'type': - if ( - tag.list && - Array.isArray(tag.list) && - tag.list.some( - (listItem: { code: string; value: string }) => - listItem.code === 'type' && listItem.value === 'item', - ) - ) { - if (!item.time) { - const key = `prvdr${i}item${j}time` - errorObj[key] = `item_id: ${item.id} should contain time object in bpp/providers[${i}]` + logger.info(`Checking items for provider (${prvdr.id}) in bpp/providers[${i}]`) + let j = 0 + const items = onSearchCatalog['bpp/providers'][i]['items'] + const iLen = items.length + while (j < iLen) { + logger.info(`Validating uniqueness for item id in bpp/providers[${i}].items[${j}]...`) + const item = items[j] + + try { + logger.info(`Validating media array in bpp/providers[${i}].items[${j}].descriptor`) + const descriptor = item.descriptor + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype is required and must be a string in media[${mediaIdx}]` } - - if (!item.category_ids) { - const key = `prvdr${i}item${j}ctgry_ids` - errorObj[key] = `item_id: ${item.id} should contain category_ids in bpp/providers[${i}]` + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url is required and must be a string in media[${mediaIdx}]` } - } - break - - case 'custom_group': - tag.list.map((it: { code: string; value: string }, index: number) => { - if (!customGrpId.has(it.value)) { - const key = `prvdr${i}item${j}tag${index}cstmgrp_id` - errorObj[key] = - `item_id: ${item.id} should have custom_group_id one of the ids passed in categories bpp/providers[${i}]` + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` } }) + } + } catch (error: any) { + logger.error(`Error while validating media array in bpp/providers[${i}].items[${j}].descriptor: ${error.stack}`) + } - break + if ('category_id' in item) { + itemCategory_id.add(item.category_id) + } - case 'config': - const idList: any = tag.list.find((item: { code: string }) => item.code === 'id') - const minList: any = tag.list.find((item: { code: string }) => item.code === 'min') - const maxList: any = tag.list.find((item: { code: string }) => item.code === 'max') - const seqList: any = tag.list.find((item: { code: string }) => item.code === 'seq') + if ('category_ids' in item) { + item[`category_ids`].map((category: string, index: number) => { + const categoryId = category.split(':')[0] + const seq = category.split(':')[1] - if (!categoriesId.has(idList.value)) { - const key = `prvdr${i}item${j}tags${index}config_list` - errorObj[key] = - `value in catalog/items${j}/tags${index}/config/list/ should be one of the catalog/category/ids` - } + // Check if seq exists in category_ids + const seqExists = item[`category_ids`].some((cat: any) => cat.seq === seq) - if (!/^-?\d+(\.\d+)?$/.test(minList.value)) { - const key = `prvdr${i}item${j}tags${index}config_min` - errorObj[key] = `Invalid value for ${minList.code}: ${minList.value}` + if (seqExists) { + const key = `prvdr${i}item${j}ctgryseq${index}` + businessErrors[key] = `duplicate seq : ${seq} in category_ids in prvdr${i}item${j}` + } else { + seqSet.add(seq) } - if (!/^-?\d+(\.\d+)?$/.test(maxList.value)) { - const key = `prvdr${i}item${j}tags${index}config_max` - errorObj[key] = `Invalid value for ${maxList.code}: ${maxList.value}` + if (!categoriesId.has(categoryId)) { + const key = `prvdr${i}item${j}ctgryId${index}` + businessErrors[key] = `item${j} should have category_ids one of the Catalog/categories/id` } + }) + } - if (!/^-?\d+(\.\d+)?$/.test(seqList.value)) { - const key = `prvdr${i}item${j}tags${index}config_seq` - errorObj[key] = `Invalid value for ${seqList.code}: ${seqList.value}` - } + let lower_and_upper_not_present: boolean = true + let default_selection_not_present: boolean = true + try { + logger.info(`Checking selling price and maximum price for item id: ${item.id}`) - break - - case 'timing': - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 5 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - const key = `prvdr${i}item${j}tags${index}timing_day` - errorObj[key] = `Invalid value for '${item.code}': ${item.value}` - } + if ('price' in item) { + const sPrice = parseFloat(item.price.value) + const maxPrice = parseFloat(item.price.maximum_value) - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - const key = `prvdr${i}item${j}tags${index}timing_time` - errorObj[key] = `Invalid time format for '${item.code}': ${item.value}` - } + const lower = parseFloat(item.price?.tags?.[0].list[0]?.value) + const upper = parseFloat(item.price?.tags?.[0].list[1]?.value) - break - default: - errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` - } + if (lower >= 0 && upper >= 0) { + lower_and_upper_not_present = false } - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + const default_selection_value = parseFloat(item.price?.tags?.[1].list[0]?.value) + const default_selection_max_value = parseFloat(item.price?.tags?.[1].list[1]?.value) - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) + if (default_selection_value >= 0 && default_selection_max_value >= 0) { + default_selection_not_present = false + } - if (dayTo < dayFrom) { - const key = `prvdr${i}item${j}tags${index}timing_dayfrom` - errorObj[key] = "'day_to' must be greater than or equal to 'day_from'" - } + if (sPrice > maxPrice) { + const key = `prvdr${i}item${j}Price` + businessErrors[key] = + `selling price of item /price/value with id: (${item.id}) can't be greater than the maximum price /price/maximum_value in /bpp/providers[${i}]/items[${j}]/` + } - if (timeTo <= timeFrom) { - const key = `prvdr${i}item${j}tags${index}timing_timefrom` - errorObj[key] = "'time_to' must be greater than 'time_from'" - } + if (upper < lower) { + const key = `prvdr${i}item${j}price/tags/` + businessErrors[key] = + `selling lower range: ${lower} of code: range with id: (${item.id}) can't be greater than the upper range : ${upper} ` } - break + if (default_selection_max_value < default_selection_value) { + const key = `prvdr${i}item${j}Price/tags` + businessErrors[key] = + `value : ${default_selection_value} of code: default_selection with id: (${item.id}) can't be greater than the maximum_value : ${default_selection_max_value} ` + } + } + } catch (e: any) { + logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${e.stack}`) + } - case 'veg_nonveg': - const allowedCodes = ['veg', 'non_veg', 'egg'] + try { + logger.info(`Checking fulfillment_id for item id: ${item.id}`) + if (item.fulfillment_id && !onSearchFFIdsArray[i].has(item.fulfillment_id)) { + const key = `prvdr${i}item${j}ff` + businessErrors[key] = + `fulfillment_id in /bpp/providers[${i}]/items[${j}] should map to one of the fulfillments id in bpp/prvdr${i}/fulfillments ` + } + } catch (e: any) { + logger.error(`Error while checking fulfillment_id for item id: ${item.id}, ${e.stack}`) + } - for (const it of tag.list) { - if (it.code && !allowedCodes.includes(it.code)) { - const key = `prvdr${i}item${j}tag${index}veg_nonveg` - errorObj[key] = - `item_id: ${item.id} should have veg_nonveg one of the 'veg', 'non_veg'or 'egg' in bpp/providers[${i}]` - } - } + try { + logger.info(`Checking location_id for item id: ${item.id}`) - break + if (item.location_id && !prvdrLocId.has(item.location_id)) { + const key = `prvdr${i}item${j}loc` + businessErrors[key] = + `location_id in /bpp/providers[${i}]/items[${j}] should be one of the locations id in /bpp/providers[${i}]/locations` + } + } catch (e: any) { + logger.error(`Error while checking location_id for item id: ${item.id}, ${e.stack}`) } - }) - } catch (e: any) { - logger.error(`Error while checking tags for item id: ${item.id}, ${e.stack}`) - } - //Validating Offers - try { - logger.info(`Checking offers.tags under bpp/providers`) - // Iterate through bpp/providers - for (let i in onSearchCatalog['bpp/providers']) { - const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? null - if (!offers) { - offers.forEach((offer: any, offerIndex: number) => { - const tags = offer.tags - - // Ensure tags exist - if (!tags || !Array.isArray(tags)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` - errorObj[key] = - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` - logger.error( - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, - ) - return - } - const metaTagsError = validateMetaTags(tags) - if (metaTagsError) { - let i = 0 - const len = metaTagsError.length - while (i < len) { - const key = `fulfilmntRngErr${i}` - errorObj[key] = `${metaTagsError[i]}` - i++ + try { + logger.info(`Checking consumer care details for item id: ${item.id}`) + if ('@ondc/org/contact_details_consumer_care' in item) { + let consCare = item['@ondc/org/contact_details_consumer_care'] + consCare = consCare.split(',') + if (consCare.length < 3) { + const key = `prvdr${i}consCare` + businessErrors[key] = + `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` + } else { + const checkEmail: boolean = emailRegex(consCare[1].trim()) + if (isNaN(consCare[2].trim()) || !checkEmail) { + const key = `prvdr${i}consCare` + businessErrors[key] = + `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` } } + } + } catch (e: any) { + logger.error(`Error while checking consumer care details for item id: ${item.id}, ${e.stack}`) + } - // Validate based on offer type - switch (offer.descriptor?.code) { - case 'discount': - // Validate 'qualifier' - const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') + try { + item.tags.map((tag: { code: any; list: any[] }, index: number) => { + switch (tag.code) { + case 'type': if ( - !qualifierDiscount || - !qualifierDiscount.list.some((item: any) => item.code === 'min_value') + tag.list && + Array.isArray(tag.list) && + tag.list.some( + (listItem: { code: string; value: string }) => + listItem.code === 'type' && listItem.value === 'item', + ) ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + if (!item.time) { + const key = `prvdr${i}item${j}time` + businessErrors[key] = `item_id: ${item.id} should contain time object in bpp/providers[${i}]` + } - // Validate 'benefit' - const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitDiscount || - !benefitDiscount.list.some((item: any) => item.code === 'value') || - !benefitDiscount.list.some((item: any) => item.code === 'value_type') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}]`, - ) + if (!item.category_ids) { + const key = `prvdr${i}item${j}ctgry_ids` + businessErrors[key] = `item_id: ${item.id} should contain category_ids in bpp/providers[${i}]` + } } break - case 'buyXgetY': - // Validate 'qualifier' - const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierBuyXgetY || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`, - ) - } + case 'custom_group': + tag.list.map((it: { code: string; value: string }, index: number) => { + if (!customGrpId.has(it.value)) { + const key = `prvdr${i}item${j}tag${index}cstmgrp_id` + businessErrors[key] = + `item_id: ${item.id} should have custom_group_id one of the ids passed in categories bpp/providers[${i}]` + } + }) - // Validate 'benefit' - const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') - if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) - } break - case 'freebie': - // Validate 'qualifier' - const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + case 'config': + const idList: any = tag.list.find((item: { code: string }) => item.code === 'id') + const minList: any = tag.list.find((item: { code: string }) => item.code === 'min') + const maxList: any = tag.list.find((item: { code: string }) => item.code === 'max') + const seqList: any = tag.list.find((item: { code: string }) => item.code === 'seq') + + if (!categoriesId.has(idList.value)) { + const key = `prvdr${i}item${j}tags${index}config_list` + businessErrors[key] = + `value in catalog/items${j}/tags${index}/config/list/ should be one of the catalog/category/ids` } - // Validate 'benefit' - const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitFreebie || - !benefitFreebie.list.some((item: any) => item.code === 'item_count') || - !benefitFreebie.list.some((item: any) => item.code === 'item_id') || - !benefitFreebie.list.some((item: any) => item.code === 'item_value') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, - ) + if (!/^-?\d+(\.\d+)?$/.test(minList.value)) { + const key = `prvdr${i}item${j}tags${index}config_min` + businessErrors[key] = `Invalid value for ${minList.code}: ${minList.value}` } - break - case 'slab': - // Validate 'qualifier' - const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + if (!/^-?\d+(\.\d+)?$/.test(maxList.value)) { + const key = `prvdr${i}item${j}tags${index}config_max` + businessErrors[key] = `Invalid value for ${maxList.code}: ${maxList.value}` } - // Validate 'benefit' - const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitSlab || - !benefitSlab.list.some((item: any) => item.code === 'value') || - !benefitSlab.list.some((item: any) => item.code === 'value_type') || - !benefitSlab.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) + if (!/^-?\d+(\.\d+)?$/.test(seqList.value)) { + const key = `prvdr${i}item${j}tags${index}config_seq` + businessErrors[key] = `Invalid value for ${seqList.code}: ${seqList.value}` } + break - case 'combo': - // Validate 'qualifier' - const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierCombo || - !qualifierCombo.list.some((item: any) => item.code === 'min_value') || - !qualifierCombo.list.some((item: any) => item.code === 'item_id') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) + case 'timing': + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 5 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + const key = `prvdr${i}item${j}tags${index}timing_day` + businessErrors[key] = `Invalid value for '${item.code}': ${item.value}` + } + + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + const key = `prvdr${i}item${j}tags${index}timing_time` + businessErrors[key] = `Invalid time format for '${item.code}': ${item.value}` + } + + break + default: + errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` + } } - // Validate 'benefit' - const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitCombo || - !benefitCombo.list.some((item: any) => item.code === 'value') || - !benefitCombo.list.some((item: any) => item.code === 'value_type') || - !benefitCombo.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) + + if (dayTo < dayFrom) { + const key = `prvdr${i}item${j}tags${index}timing_dayfrom` + businessErrors[key] = "'day_to' must be greater than or equal to 'day_from'" + } + + if (timeTo <= timeFrom) { + const key = `prvdr${i}item${j}tags${index}timing_timefrom` + businessErrors[key] = "'time_to' must be greater than 'time_from'" + } } + break - case 'delivery': - // Validate 'qualifier' - const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierDelivery || - !qualifierDelivery.list.some((item: any) => item.code === 'min_value') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + case 'veg_nonveg': + const allowedCodes = ['veg', 'non_veg', 'egg'] + + for (const it of tag.list) { + if (it.code && !allowedCodes.includes(it.code)) { + const key = `prvdr${i}item${j}tag${index}veg_nonveg` + businessErrors[key] = + `item_id: ${item.id} should have veg_nonveg one of the 'veg', 'non_veg'or 'egg' in bpp/providers[${i}]` + } } - // Validate 'benefit' - const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitDelivery || - !benefitDelivery.list.some((item: any) => item.code === 'value') || - !benefitDelivery.list.some((item: any) => item.code === 'value_type') || - !benefitDelivery.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + break + } + }) + } catch (e: any) { + logger.error(`Error while checking tags for item id: ${item.id}, ${e.stack}`) + } + //Validating Offers + try { + logger.info(`Checking offers.tags under bpp/providers`) + + // Iterate through bpp/providers + for (let i in onSearchCatalog['bpp/providers']) { + const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? null + if (!offers) { + offers.forEach((offer: any, offerIndex: number) => { + const tags = offer.tags + + // Ensure tags exist + if (!tags || !Array.isArray(tags)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` + businessErrors[key] = + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, ) + return + } + const metaTagsError = validateMetaTags(tags) + if (metaTagsError) { + let i = 0 + const len = metaTagsError.length + while (i < len) { + const key = `fulfilmntRngErr${i}` + businessErrors[key] = `${metaTagsError[i]}` + i++ + } } - break - // case 'exchange': - case 'financing': - // Validate 'qualifier' - // const qualifierExchangeFinancing = tags.find((tag: any) => tag.code === 'qualifier'); - // if (!qualifierExchangeFinancing || !qualifierExchangeFinancing.list.some((item: any) => item.code === 'min_value')) { - // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - // errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - // } - - // // Validate that benefits should not exist or should be empty - // const benefitExchangeFinancing = tags.find((tag: any) => tag.code === 'benefit'); - // if (benefitExchangeFinancing && benefitExchangeFinancing.list.length > 0) { - // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - // errorObj[key] = `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - // logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`); - // } - break + // Validate based on offer type + switch (offer.descriptor?.code) { + case 'discount': + // Validate 'qualifier' + const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierDiscount || + !qualifierDiscount.list.some((item: any) => item.code === 'min_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - // No validation for benefits as it is not required - // break; + // Validate 'benefit' + const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitDiscount || + !benefitDiscount.list.some((item: any) => item.code === 'value') || + !benefitDiscount.list.some((item: any) => item.code === 'value_type') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}]`, + ) + } + break + + case 'buyXgetY': + // Validate 'qualifier' + const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierBuyXgetY || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`, + ) + } - default: - logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) + // Validate 'benefit' + const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') + if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) + } + break + + case 'freebie': + // Validate 'qualifier' + const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } + + // Validate 'benefit' + const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitFreebie || + !benefitFreebie.list.some((item: any) => item.code === 'item_count') || + !benefitFreebie.list.some((item: any) => item.code === 'item_id') || + !benefitFreebie.list.some((item: any) => item.code === 'item_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, + ) + } + break + + case 'slab': + // Validate 'qualifier' + const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } + + // Validate 'benefit' + const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitSlab || + !benefitSlab.list.some((item: any) => item.code === 'value') || + !benefitSlab.list.some((item: any) => item.code === 'value_type') || + !benefitSlab.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) + } + break + + case 'combo': + // Validate 'qualifier' + const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierCombo || + !qualifierCombo.list.some((item: any) => item.code === 'min_value') || + !qualifierCombo.list.some((item: any) => item.code === 'item_id') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) + } + + // Validate 'benefit' + const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitCombo || + !benefitCombo.list.some((item: any) => item.code === 'value') || + !benefitCombo.list.some((item: any) => item.code === 'value_type') || + !benefitCombo.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) + } + break + + case 'delivery': + // Validate 'qualifier' + const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierDelivery || + !qualifierDelivery.list.some((item: any) => item.code === 'min_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } + + // Validate 'benefit' + const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitDelivery || + !benefitDelivery.list.some((item: any) => item.code === 'value') || + !benefitDelivery.list.some((item: any) => item.code === 'value_type') || + !benefitDelivery.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) + } + break + + // case 'exchange': + case 'financing': + // Validate 'qualifier' + // const qualifierExchangeFinancing = tags.find((tag: any) => tag.code === 'qualifier'); + // if (!qualifierExchangeFinancing || !qualifierExchangeFinancing.list.some((item: any) => item.code === 'min_value')) { + // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + // businessErrors[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + // } + + // // Validate that benefits should not exist or should be empty + // const benefitExchangeFinancing = tags.find((tag: any) => tag.code === 'benefit'); + // if (benefitExchangeFinancing && benefitExchangeFinancing.list.length > 0) { + // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + // businessErrors[key] = `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + // logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`); + // } + break + + // No validation for benefits as it is not required + // break; + + default: + logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) + } + }) } - }) + } + } catch (error: any) { + logger.error(`Error while checking offers.tags under bpp/providers: ${error.stack}`) + } + + // false error coming from here + try { + logger.info(`Validating item tags`) + const itemTypeTag = item.tags.find((tag: { code: string }) => tag.code === 'type') + const customGroupTag = item.tags.find((tag: { code: string }) => tag.code === 'custom_group') + if (itemTypeTag && itemTypeTag.list.length > 0 && itemTypeTag.list[0].value === 'item' && !customGroupTag) { + businessErrors[`items[${item.id}]`] = + `/message/catalog/bpp/providers/items/tags/'type' is optional for non-customizable (standalone) SKUs` + } else if ( + itemTypeTag && + itemTypeTag.list.length > 0 && + itemTypeTag.list[0].value === 'item' && + customGroupTag + ) { + if (default_selection_not_present) { + businessErrors[`items[${item.id}]/price/tags/default_selection`] = + `/message/catalog/bpp/providers/items must have default_selection price for customizable items` + } + if (lower_and_upper_not_present) { + businessErrors[`items[${item.id}]/price/tags/lower_and_upper_range`] = + `/message/catalog/bpp/providers/items must have lower/upper range for customizable items` + } + } + } catch (error: any) { + logger.error(`Error while validating item, ${error.stack}`) + } + + try { + logger.info(`Validating default customizations`) + const itemTypeTag = item.tags.find( + (tag: any) => + tag.code === 'type' && + tag.list.some((item: any) => item.code === 'type' && item.value === 'customization'), + ) + if (itemTypeTag) { + const parentTag = item.tags.find((tag: any) => tag.code === 'parent') + if (parentTag) { + const categoryId = parentTag.list.find((item: any) => item.code === 'id')?.value + if (categoryId) { + const category = categories.find((category: any) => category.id === categoryId) + if (category) { + const configTag = category.tags.find((tag: any) => tag.code === 'config') + if (configTag) { + const minSelection = configTag.list.find((item: any) => item.code === 'min')?.value + if (minSelection === '0') { + const defaultTag = parentTag.list.find((item: any) => item.code === 'default') + if (defaultTag && defaultTag.value === 'yes') { + businessErrors[`items[${item.id}]category[${categoryId}]`] = + `Default customization should not be set true for a custom_group where min selection is 0` + } + } + } + } + } + } + } + } catch (error: any) { + logger.error(`Error while validating default customizations, ${error.stack}`) } + + j++ } } catch (error: any) { - logger.error(`Error while checking offers.tags under bpp/providers: ${error.stack}`) + logger.error(`!!Errors while checking items in bpp/providers[${i}], ${error.stack}`) } - // false error coming from here try { - logger.info(`Validating item tags`) - const itemTypeTag = item.tags.find((tag: { code: string }) => tag.code === 'type') - const customGroupTag = item.tags.find((tag: { code: string }) => tag.code === 'custom_group') - if (itemTypeTag && itemTypeTag.list.length > 0 && itemTypeTag.list[0].value === 'item' && !customGroupTag) { - errorObj[`items[${item.id}]`] = - `/message/catalog/bpp/providers/items/tags/'type' is optional for non-customizable (standalone) SKUs` - } else if ( - itemTypeTag && - itemTypeTag.list.length > 0 && - itemTypeTag.list[0].value === 'item' && - customGroupTag - ) { - if (default_selection_not_present) { - errorObj[`items[${item.id}]/price/tags/default_selection`] = - `/message/catalog/bpp/providers/items must have default_selection price for customizable items` - } - if (lower_and_upper_not_present) { - errorObj[`items[${item.id}]/price/tags/lower_and_upper_range`] = - `/message/catalog/bpp/providers/items must have lower/upper range for customizable items` + const providers = data.message.catalog['bpp/providers'] + const address = providers[0].locations[0].address + + if (address) { + const area_code = Number.parseInt(address.area_code) + const std = typeof context.city === 'string' && context.city.includes(':') + ? context.city.split(':')[1] + : undefined + + logger.info(`Comparing area_code and STD Code for /${constants.ON_SEARCH}`) + const areaWithSTD = compareSTDwithArea(area_code, std) + if (!areaWithSTD) { + logger.error(`STD code does not match with correct area_code in /${constants.ON_SEARCH}`) + errorObj.invldAreaCode = `STD code does not match with correct area_code in /${constants.ON_SEARCH}` } } } catch (error: any) { - logger.error(`Error while validating item, ${error.stack}`) + logger.error( + `Error while matching area_code and std code for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + ) } + // Compairing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp try { - logger.info(`Validating default customizations`) - const itemTypeTag = item.tags.find( - (tag: any) => - tag.code === 'type' && - tag.list.some((item: any) => item.code === 'type' && item.value === 'customization'), - ) - if (itemTypeTag) { - const parentTag = item.tags.find((tag: any) => tag.code === 'parent') - if (parentTag) { - const categoryId = parentTag.list.find((item: any) => item.code === 'id')?.value - if (categoryId) { - const category = categories.find((category: any) => category.id === categoryId) - if (category) { - const configTag = category.tags.find((tag: any) => tag.code === 'config') - if (configTag) { - const minSelection = configTag.list.find((item: any) => item.code === 'min')?.value - if (minSelection === '0') { - const defaultTag = parentTag.list.find((item: any) => item.code === 'default') - if (defaultTag && defaultTag.value === 'yes') { - errorObj[`items[${item.id}]category[${categoryId}]`] = - `Default customization should not be set true for a custom_group where min selection is 0` - } - } - } + logger.info(`Compairing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp`) + const timestamp = context.timestamp + for (let i in onSearchCatalog['bpp/providers']) { + const items = onSearchCatalog['bpp/providers'][i].items + items.forEach((item: any, index: number) => { + if (item?.time) { + const itemTimeStamp = item.time.timestamp + const op = areTimestampsLessThanOrEqualTo(itemTimeStamp, timestamp) + if (!op) { + const key = `bpp/providers/items/time/timestamp[${index}]` + businessErrors[key] = `Timestamp for item[${index}] can't be greater than context.timestamp` + logger.error(`Timestamp for item[${index}] can't be greater than context.timestamp`) } } - } + }) } } catch (error: any) { - logger.error(`Error while validating default customizations, ${error.stack}`) + logger.error( + `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, + ) } - j++ - } - } catch (error: any) { - logger.error(`!!Errors while checking items in bpp/providers[${i}], ${error.stack}`) - } + try { + logger.info(`Checking for tags array in message/catalog/bpp/providers[0]/categories[0]/tags`) + const categories = message.catalog['bpp/providers'][i].categories + categories.forEach((item: any) => { + const tags = item.tags + if (tags.length < 1) { + const key = `message/catalog/bpp/providers/categories` + businessErrors[key] = `/message/catalog/bpp/providers[${i}]/categories cannot have tags as an empty array` + } + }) + } catch (error: any) { + logger.error(`Error while checking tags array in message/catalog/bpp/providers[${i}]/categories`) + } - try { - const providers = data.message.catalog['bpp/providers'] - const address = providers[0].locations[0].address + try { + let customMenus = [] + customMenus = categories.filter((category: any) => + category.tags.some( + (tag: any) => tag.code === 'type' && tag.list.some((type: any) => type.value === 'custom_menu'), + ), + ) - if (address) { - const area_code = Number.parseInt(address.area_code) - const std = context.city.split(':')[1] + if (customMenus.length > 0) { + customMenu = true - logger.info(`Comparing area_code and STD Code for /${constants.ON_SEARCH}`) - const areaWithSTD = compareSTDwithArea(area_code, std) - if (!areaWithSTD) { - logger.error(`STD code does not match with correct area_code in /${constants.ON_SEARCH}`) - errorObj.invldAreaCode = `STD code does not match with correct area_code in /${constants.ON_SEARCH}` - } - } - } catch (error: any) { - logger.error( - `Error while matching area_code and std code for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } + const ranks = customMenus.map((cstmMenu: any) => + parseInt( + cstmMenu.tags + .find((tag: any) => tag.code === 'display') + .list.find((display: any) => display.code === 'rank').value, + ), + ) - // Compairing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp - try { - logger.info(`Compairing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp`) - const timestamp = context.timestamp - for (let i in onSearchCatalog['bpp/providers']) { - const items = onSearchCatalog['bpp/providers'][i].items - items.forEach((item: any, index: number) => { - if (item?.time) { - const itemTimeStamp = item.time.timestamp - const op = areTimestampsLessThanOrEqualTo(itemTimeStamp, timestamp) - if (!op) { - const key = `bpp/providers/items/time/timestamp[${index}]` - errorObj[key] = `Timestamp for item[${index}] can't be greater than context.timestamp` - logger.error(`Timestamp for item[${index}] can't be greater than context.timestamp`) + // Check for duplicates and missing ranks + const hasDuplicates = ranks.length !== new Set(ranks).size + const missingRanks = [...Array(Math.max(...ranks)).keys()] + .map((i) => i + 1) + .filter((rank) => !ranks.includes(rank)) + + if (hasDuplicates) { + const key = `message/catalog/bpp/providers${i}/categories/ranks` + businessErrors[key] = `Duplicate ranks found, ${ranks} in providers${i}/categories` + logger.error(`Duplicate ranks found, ${ranks} in providers${i}/categories`) + } else if (missingRanks.length > 0) { + const key = `message/catalog/bpp/providers${i}/categories/ranks` + businessErrors[key] = `Missing ranks:, ${missingRanks} in providers${i}/categories` + logger.error(`Missing ranks:, ${missingRanks} in providers${i}/categories`) + } else { + // Sort customMenus by rank + const sortedCustomMenus = customMenus.sort((a: any, b: any) => { + const rankA = parseInt( + a.tags.find((tag: any) => tag.code === 'display').list.find((display: any) => display.code === 'rank') + .value, + ) + const rankB = parseInt( + b.tags.find((tag: any) => tag.code === 'display').list.find((display: any) => display.code === 'rank') + .value, + ) + return rankA - rankB + }) + + // Extract IDs + customMenuIds = sortedCustomMenus.map((item: any) => item.id) } } - }) - } - } catch (error: any) { - logger.error( - `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, - ) - } - - try { - logger.info(`Checking for tags array in message/catalog/bpp/providers[0]/categories[0]/tags`) - const categories = message.catalog['bpp/providers'][i].categories - categories.forEach((item: any) => { - const tags = item.tags - if (tags.length < 1) { - const key = `message/catalog/bpp/providers/categories` - errorObj[key] = `/message/catalog/bpp/providers[${i}]/categories cannot have tags as an empty array` + } catch (error: any) { + logger.error(`!!Errors while checking rank in bpp/providers[${i}].category.tags, ${error.stack}`) } - }) - } catch (error: any) { - logger.error(`Error while checking tags array in message/catalog/bpp/providers[${i}]/categories`) - } - - try { - let customMenus = [] - customMenus = categories.filter((category: any) => - category.tags.some( - (tag: any) => tag.code === 'type' && tag.list.some((type: any) => type.value === 'custom_menu'), - ), - ) - - if (customMenus.length > 0) { - customMenu = true - - const ranks = customMenus.map((cstmMenu: any) => - parseInt( - cstmMenu.tags - .find((tag: any) => tag.code === 'display') - .list.find((display: any) => display.code === 'rank').value, - ), - ) - - // Check for duplicates and missing ranks - const hasDuplicates = ranks.length !== new Set(ranks).size - const missingRanks = [...Array(Math.max(...ranks)).keys()] - .map((i) => i + 1) - .filter((rank) => !ranks.includes(rank)) - - if (hasDuplicates) { - const key = `message/catalog/bpp/providers${i}/categories/ranks` - errorObj[key] = `Duplicate ranks found, ${ranks} in providers${i}/categories` - logger.error(`Duplicate ranks found, ${ranks} in providers${i}/categories`) - } else if (missingRanks.length > 0) { - const key = `message/catalog/bpp/providers${i}/categories/ranks` - errorObj[key] = `Missing ranks:, ${missingRanks} in providers${i}/categories` - logger.error(`Missing ranks:, ${missingRanks} in providers${i}/categories`) - } else { - // Sort customMenus by rank - const sortedCustomMenus = customMenus.sort((a: any, b: any) => { - const rankA = parseInt( - a.tags.find((tag: any) => tag.code === 'display').list.find((display: any) => display.code === 'rank') - .value, - ) - const rankB = parseInt( - b.tags.find((tag: any) => tag.code === 'display').list.find((display: any) => display.code === 'rank') - .value, - ) - return rankA - rankB - }) + if (customMenu) { + try { + const categoryMap: Record = {} + onSearchCatalog['bpp/providers'][i]['items'].forEach((item: any) => { + if (item?.category_ids) { + item?.category_ids?.forEach((category_id: any) => { + const [category, sequence] = category_id.split(':').map(Number) + if (!categoryMap[category]) { + categoryMap[category] = [] + } + categoryMap[category].push(sequence) + }) - // Extract IDs - customMenuIds = sortedCustomMenus.map((item: any) => item.id) - } - } - } catch (error: any) { - logger.error(`!!Errors while checking rank in bpp/providers[${i}].category.tags, ${error.stack}`) - } - if (customMenu) { - try { - const categoryMap: Record = {} - onSearchCatalog['bpp/providers'][i]['items'].forEach((item: any) => { - if (item?.category_ids) { - item?.category_ids?.forEach((category_id: any) => { - const [category, sequence] = category_id.split(':').map(Number) - if (!categoryMap[category]) { - categoryMap[category] = [] + // Sort the sequences for each category + Object.keys(categoryMap).forEach((category) => { + categoryMap[category].sort((a, b) => a - b) + }) } - categoryMap[category].push(sequence) - }) - - // Sort the sequences for each category - Object.keys(categoryMap).forEach((category) => { - categoryMap[category].sort((a, b) => a - b) }) - } - }) - let countSeq = 0 - - customMenuIds.map((category_id: any) => { - const categoryArray = categoryMap[`${category_id}`] - if (!categoryArray) { - const key = `message/catalog/bpp/providers${i}/categories/items` - errorObj[key] = `No items are mapped with the given category_id ${category_id} in providers${i}/items` - logger.error(`No items are mapped with the given category_id ${category_id} in providers${i}/items`) - } else { - let i = 0 - while (i < categoryArray.length) { - countSeq++ - const exist = categoryArray.includes(countSeq) - if (!exist) { - const key = `providers${i}/categories/items/${countSeq}` - errorObj[key] = - `The given sequence ${countSeq} doesn't exist with with the given category_id ${category_id} in providers${i}/items according to the rank` - logger.error( - `The given sequence ${countSeq} doesn't exist with with the given category_id ${category_id} in providers${i}/items according to the rank`, - ) + let countSeq = 0 + + customMenuIds.map((category_id: any) => { + const categoryArray = categoryMap[`${category_id}`] + if (!categoryArray) { + const key = `message/catalog/bpp/providers${i}/categories/items` + businessErrors[key] = `No items are mapped with the given category_id ${category_id} in providers${i}/items` + logger.error(`No items are mapped with the given category_id ${category_id} in providers${i}/items`) + } else { + let i = 0 + while (i < categoryArray.length) { + countSeq++ + const exist = categoryArray.includes(countSeq) + if (!exist) { + const key = `providers${i}/categories/items/${countSeq}` + businessErrors[key] = + `The given sequence ${countSeq} doesn't exist with with the given category_id ${category_id} in providers${i}/items according to the rank` + logger.error( + `The given sequence ${countSeq} doesn't exist with with the given category_id ${category_id} in providers${i}/items according to the rank`, + ) + } + i++ + } } - i++ - } + }) + } catch (error: any) { + logger.error(`!!Errors while category_ids in the items, ${error.stack}`) } - }) - } catch (error: any) { - logger.error(`!!Errors while category_ids in the items, ${error.stack}`) - } - } + } - // Checking image array for bpp/providers/[]/categories/[]/descriptor/images[] - try { - logger.info(`Checking image array for bpp/provider/categories/descriptor/images[]`) - for (let i in onSearchCatalog['bpp/providers']) { - const categories = onSearchCatalog['bpp/providers'][i].categories - categories.forEach((item: any, index: number) => { - if (item.descriptor.images && item.descriptor.images.length < 1) { - const key = `bpp/providers[${i}]/categories[${index}]/descriptor` - errorObj[key] = `Images should not be provided as empty array for categories[${index}]/descriptor` - logger.error(`Images should not be provided as empty array for categories[${index}]/descriptor`) + // Checking image array for bpp/providers/[]/categories/[]/descriptor/images[] + try { + logger.info(`Checking image array for bpp/provider/categories/descriptor/images[]`) + for (let i in onSearchCatalog['bpp/providers']) { + const categories = onSearchCatalog['bpp/providers'][i].categories + categories.forEach((item: any, index: number) => { + if (item.descriptor.images && item.descriptor.images.length < 1) { + const key = `bpp/providers[${i}]/categories[${index}]/descriptor` + businessErrors[key] = `Images should not be provided as empty array for categories[${index}]/descriptor` + logger.error(`Images should not be provided as empty array for categories[${index}]/descriptor`) + } + }) } - }) - } - } catch (error: any) { - logger.error( - `!!Errors while checking image array for bpp/providers/[]/categories/[]/descriptor/images[], ${error.stack}`, - ) - } - - // Checking for same parent_item_id - // try { - // logger.info(`Checking for duplicate varient in bpp/providers/items for on_search`) - // for (let i in onSearchCatalog['bpp/providers']) { - // const items = onSearchCatalog['bpp/providers'][i].items - // const map = checkDuplicateParentIdItems(items) - // for (let key in map) { - // if (map[key].length > 1) { - // const measures = map[key].map((item: any) => { - // const unit = item.quantity.unitized.measure.unit - // const value = parseInt(item.quantity.unitized.measure.value) - // return { unit, value } - // }) - // checkForDuplicates(measures, errorObj) - // } - // } - // } - // } catch (error: any) { - // logger.error( - // `!!Errors while checking parent_item_id in bpp/providers/[]/items/[]/parent_item_id/, ${error.stack}`, - // ) - // } - - // servicability Construct - try { - logger.info(`Checking serviceability construct for bpp/providers[${i}]`) - - const tags = onSearchCatalog['bpp/providers'][i]['tags'] - if (!tags || !tags.length) { - const key = `prvdr${i}tags` - errorObj[key] = `tags must be present in bpp/providers[${i}]` - } - if (tags) { - const circleRequired = checkServiceabilityType(tags) - if (circleRequired) { - const errors = validateLocations(message.catalog['bpp/providers'][i].locations, tags) - errorObj = { ...errorObj, ...errors } + } catch (error: any) { + logger.error( + `!!Errors while checking image array for bpp/providers/[]/categories/[]/descriptor/images[], ${error.stack}`, + ) } - } - //checking for each serviceability construct and matching serviceability constructs with the previous ones - const serviceabilitySet = new Set() - const timingSet = new Set() - tags.forEach((sc: any, t: any) => { - if (sc.code === 'serviceability') { - if (serviceabilitySet.has(JSON.stringify(sc))) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous serviceability constructs` - } + // Checking for same parent_item_id + // try { + // logger.info(`Checking for duplicate varient in bpp/providers/items for on_search`) + // for (let i in onSearchCatalog['bpp/providers']) { + // const items = onSearchCatalog['bpp/providers'][i].items + // const map = checkDuplicateParentIdItems(items) + // for (let key in map) { + // if (map[key].length > 1) { + // const measures = map[key].map((item: any) => { + // const unit = item.quantity.unitized.measure.unit + // const value = parseInt(item.quantity.unitized.measure.value) + // return { unit, value } + // }) + // checkForDuplicates(measures, errorObj) + // } + // } + // } + // } catch (error: any) { + // logger.error( + // `!!Errors while checking parent_item_id in bpp/providers/[]/items/[]/parent_item_id/, ${error.stack}`, + // ) + // } + + // servicability Construct + try { + logger.info(`Checking serviceability construct for bpp/providers[${i}]`) - serviceabilitySet.add(JSON.stringify(sc)) - if ('list' in sc) { - if (sc.list.length < 5) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract` + const tags = onSearchCatalog['bpp/providers'][i]['tags'] + if (!tags || !tags.length) { + const key = `prvdr${i}tags` + businessErrors[key] = `tags must be present in bpp/providers[${i}]` + } + if (tags) { + const circleRequired = checkServiceabilityType(tags) + if (circleRequired) { + const errors = validateLocations(message.catalog['bpp/providers'][i].locations, tags) + Object.assign(businessErrors, errors) } + } - //checking location - const loc = sc.list.find((elem: any) => elem.code === 'location') || '' - if (!loc) { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` - } else { - if ('value' in loc) { - if (!prvdrLocId.has(loc.value)) { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `location in serviceability construct should be one of the location ids bpp/providers[${i}]/locations` - } - } else { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` + //checking for each serviceability construct and matching serviceability constructs with the previous ones + const serviceabilitySet = new Set() + const timingSet = new Set() + tags.forEach((sc: any, t: any) => { + if (sc.code === 'serviceability') { + if (serviceabilitySet.has(JSON.stringify(sc))) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous serviceability constructs` } - } - //checking category - const ctgry = sc.list.find((elem: any) => elem.code === 'category') || '' - if (!ctgry) { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` - } else { - if ('value' in ctgry) { - if (!itemCategory_id.has(ctgry.value)) { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `category in serviceability construct should be one of the category ids bpp/providers[${i}]/items/category_id` + serviceabilitySet.add(JSON.stringify(sc)) + if ('list' in sc) { + if (sc.list.length < 5) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract` } - } else { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` - } - } - //checking type (hyperlocal, intercity or PAN India) - const type = sc.list.find((elem: any) => elem.code === 'type') || '' - if (!type) { - const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` - } else { - if ('value' in type) { - switch (type.value) { - case '10': - { - //For hyperlocal - - //checking value - const val = sc.list.find((elem: any) => elem.code === 'val') || '' - if ('value' in val) { - if (isNaN(val.value)) { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be a number (code:"val") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` - } - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } - - //checking unit - const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' - if ('value' in unit) { - if (unit.value != 'km') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "km" (code:"unit") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` - } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } + //checking location + const loc = sc.list.find((elem: any) => elem.code === 'location') || '' + if (!loc) { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` + } else { + if ('value' in loc) { + if (!prvdrLocId.has(loc.value)) { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `location in serviceability construct should be one of the location ids bpp/providers[${i}]/locations` } + } else { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` + } + } - break - case '11': - { - //intercity - - //checking value - const val = sc.list.find((elem: any) => elem.code === 'val') || '' - if ('value' in val) { - const pincodes = val.value.split(/,|-/) - pincodes.forEach((pincode: any) => { - if (isNaN(pincode) || pincode.length != 6) { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be a valid range of pincodes (code:"val") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` - } - }) - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } - - //checking unit - const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' - if ('value' in unit) { - if (unit.value != 'pincode') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "pincode" (code:"unit") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` - } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } + //checking category + const ctgry = sc.list.find((elem: any) => elem.code === 'category') || '' + if (!ctgry) { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` + } else { + if ('value' in ctgry) { + if (!itemCategory_id.has(ctgry.value)) { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `category in serviceability construct should be one of the category ids bpp/providers[${i}]/items/category_id` } + } else { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` + } + } - break - case '12': - { - //PAN India - - //checking value - const val = sc.list.find((elem: any) => elem.code === 'val') || '' - if ('value' in val) { - if (val.value != 'IND') { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be "IND" (code:"val") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` - } - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } + //checking type (hyperlocal, intercity or PAN India) + const type = sc.list.find((elem: any) => elem.code === 'type') || '' + if (!type) { + const key = `prvdr${i}tags${t}type` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` + } else { + if ('value' in type) { + switch (type.value) { + case '10': + { + //For hyperlocal + + //checking value + const val = sc.list.find((elem: any) => elem.code === 'val') || '' + if ('value' in val) { + if (isNaN(val.value)) { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be a number (code:"val") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } - //checking unit - const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' - if ('value' in unit) { - if (unit.value != 'country') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "country" (code:"unit") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + //checking unit + const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' + if ('value' in unit) { + if (unit.value != 'km') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "km" (code:"unit") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } - } - break - case '13': { - // Polygon-based serviceability - - // Validate 'val' (GeoJSON) - const val = sc.list.find((elem: any) => elem.code === 'val') || '' - if ('value' in val) { - let geojson - try { - geojson = JSON.parse(val.value) - - const isFeatureCollection = - geojson.type === 'FeatureCollection' && Array.isArray(geojson.features) - if (!isFeatureCollection) { - throw new Error('Invalid GeoJSON type or missing features array') + + break + case '11': + { + //intercity + + //checking value + const val = sc.list.find((elem: any) => elem.code === 'val') || '' + if ('value' in val) { + const pincodes = val.value.split(/,|-/) + pincodes.forEach((pincode: any) => { + if (isNaN(pincode) || pincode.length != 6) { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be a valid range of pincodes (code:"val") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` + } + }) + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } + + //checking unit + const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' + if ('value' in unit) { + if (unit.value != 'pincode') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "pincode" (code:"unit") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } } - for (const feature of geojson.features) { - const geom = feature.geometry - if (!geom || !geom.type || !Array.isArray(geom.coordinates)) { - throw new Error('Invalid feature geometry') + break + case '12': + { + //PAN India + + //checking value + const val = sc.list.find((elem: any) => elem.code === 'val') || '' + if ('value' in val) { + if (val.value != 'IND') { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be "IND" (code:"val") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` } - const checkCoordinates = (coords: any) => { - for (const polygon of coords) { - if (!Array.isArray(polygon)) throw new Error('Invalid coordinate set') - const [first, last] = [polygon[0], polygon[polygon.length - 1]] + //checking unit + const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' + if ('value' in unit) { + if (unit.value != 'country') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "country" (code:"unit") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } + } + break + case '13': { + // Polygon-based serviceability + + // Validate 'val' (GeoJSON) + const val = sc.list.find((elem: any) => elem.code === 'val') || '' + if ('value' in val) { + let geojson + try { + geojson = JSON.parse(val.value) + + const isFeatureCollection = + geojson.type === 'FeatureCollection' && Array.isArray(geojson.features) + if (!isFeatureCollection) { + throw new Error('Invalid GeoJSON type or missing features array') + } - // Check closure - if (JSON.stringify(first) !== JSON.stringify(last)) { - throw new Error('Polygon is not closed') + for (const feature of geojson.features) { + const geom = feature.geometry + if (!geom || !geom.type || !Array.isArray(geom.coordinates)) { + throw new Error('Invalid feature geometry') } - // Check precision - for (const [lng, lat] of polygon) { - if (typeof lng !== 'number' || typeof lat !== 'number') { - throw new Error('Coordinates must be numbers') - } - - const getDecimalPlaces = (num: number) => { - const parts = num.toString().split('.') - return parts[1]?.length || 0 + const checkCoordinates = (coords: any) => { + for (const polygon of coords) { + if (!Array.isArray(polygon)) throw new Error('Invalid coordinate set') + const [first, last] = [polygon[0], polygon[polygon.length - 1]] + + // Check closure + if (JSON.stringify(first) !== JSON.stringify(last)) { + throw new Error('Polygon is not closed') + } + + // Check precision + for (const [lng, lat] of polygon) { + if (typeof lng !== 'number' || typeof lat !== 'number') { + throw new Error('Coordinates must be numbers') + } + + const getDecimalPlaces = (num: number) => { + const parts = num.toString().split('.') + return parts[1]?.length || 0 + } + + if (getDecimalPlaces(lng) < 4 || getDecimalPlaces(lat) < 4) { + throw new Error('Coordinates must have at least 4 decimal places') + } + } } + } - if (getDecimalPlaces(lng) < 4 || getDecimalPlaces(lat) < 4) { - throw new Error('Coordinates must have at least 4 decimal places') + if (geom.type === 'Polygon') { + checkCoordinates(geom.coordinates) + } else if (geom.type === 'MultiPolygon') { + for (const coords of geom.coordinates) { + checkCoordinates(coords) } + } else { + throw new Error('Unsupported geometry type') } } - } - - if (geom.type === 'Polygon') { - checkCoordinates(geom.coordinates) - } else if (geom.type === 'MultiPolygon') { - for (const coords of geom.coordinates) { - checkCoordinates(coords) + } catch (e) { + const key = `prvdr${i}tags${t}valvalue` + if (e instanceof Error) { + businessErrors[key] = + `value should be a valid GeoJSON (code:"val") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]. Error: ${e.message}` + } else { + businessErrors[key] = + `value should be a valid GeoJSON (code:"val") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]. Unknown error occurred.` } - } else { - throw new Error('Unsupported geometry type') } + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `value is missing for code "val" in type 13 (polygon) at /bpp/providers[${i}]/tags[${t}]` } - } catch (e) { - const key = `prvdr${i}tags${t}valvalue` - if (e instanceof Error) { - errorObj[key] = - `value should be a valid GeoJSON (code:"val") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]. Error: ${e.message}` + + // Validate 'unit' + const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' + if ('value' in unit) { + if (unit.value !== 'geojson') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "geojson" (code:"unit") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]` + } } else { - errorObj[key] = - `value should be a valid GeoJSON (code:"val") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]. Unknown error occurred.` + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `value is missing for code "unit" in type 13 (polygon) at /bpp/providers[${i}]/tags[${t}]` } - } - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `value is missing for code "val" in type 13 (polygon) at /bpp/providers[${i}]/tags[${t}]` - } - // Validate 'unit' - const unit = sc.list.find((elem: any) => elem.code === 'unit') || '' - if ('value' in unit) { - if (unit.value !== 'geojson') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "geojson" (code:"unit") for type 13 (polygon) in /bpp/providers[${i}]/tags[${t}]` + break + } + default: { + const key = `prvdr${i}tags${t}type` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (invalid type "${type.value}")` } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `value is missing for code "unit" in type 13 (polygon) at /bpp/providers[${i}]/tags[${t}]` } - - break - } - default: { + } else { const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (invalid type "${type.value}")` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` } } - } else { - const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` } } - } - } - if (sc.code === 'timing') { - if (timingSet.has(JSON.stringify(sc))) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `timing construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous timing constructs` - } + if (sc.code === 'timing') { + if (timingSet.has(JSON.stringify(sc))) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `timing construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous timing constructs` + } - timingSet.add(JSON.stringify(sc)) - const fulfillments = prvdr['fulfillments'] - const fulfillmentTypes = fulfillments.map((fulfillment: any) => fulfillment.type) + timingSet.add(JSON.stringify(sc)) + const fulfillments = prvdr['fulfillments'] + const fulfillmentTypes = fulfillments.map((fulfillment: any) => fulfillment.type) - let isOrderPresent = false - const typeCode = sc?.list.find((item: any) => item.code === 'type') - if (typeCode) { - const timingType = typeCode.value - if ( - timingType === 'Order' || - timingType === 'Delivery' || - timingType === 'Self-Pickup' || - timingType === 'All' - ) { - isOrderPresent = true - } else if (!fulfillmentTypes.includes(timingType)) { - errorObj[`provider[${i}].timing`] = - `The type '${timingType}' in timing tags should match with types in fulfillments array, along with 'Order'` - } - } + let isOrderPresent = false + const typeCode = sc?.list.find((item: any) => item.code === 'type') + if (typeCode) { + const timingType = typeCode.value + if ( + timingType === 'Order' || + timingType === 'Delivery' || + timingType === 'Self-Pickup' || + timingType === 'All' + ) { + isOrderPresent = true + } else if (!fulfillmentTypes.includes(timingType)) { + businessErrors[`provider[${i}].timing`] = + `The type '${timingType}' in timing tags should match with types in fulfillments array, along with 'Order'` + } + } - if (!isOrderPresent) { - errorObj[`provider[${i}].tags.timing`] = `'Order' type must be present in timing tags` + if (!isOrderPresent) { + businessErrors[`provider[${i}].tags.timing`] = `'Order' type must be present in timing tags` + } + } + }) + if (isEmpty(serviceabilitySet)) { + const key = `prvdr${i}tags/serviceability` + businessErrors[key] = `serviceability construct is mandatory in /bpp/providers[${i}]/tags` + } else if (serviceabilitySet.size != itemCategory_id.size) { + const key = `prvdr${i}/serviceability` + businessErrors[key] = + `The number of unique category_id should be equal to count of serviceability in /bpp/providers[${i}]` } - } - }) - if (isEmpty(serviceabilitySet)) { - const key = `prvdr${i}tags/serviceability` - errorObj[key] = `serviceability construct is mandatory in /bpp/providers[${i}]/tags` - } else if (serviceabilitySet.size != itemCategory_id.size) { - const key = `prvdr${i}/serviceability` - errorObj[key] = - `The number of unique category_id should be equal to count of serviceability in /bpp/providers[${i}]` - } - if (isEmpty(timingSet)) { - const key = `prvdr${i}tags/timing` - errorObj[key] = `timing construct is mandatory in /bpp/providers[${i}]/tags` - } else { - const timingsPayloadArr = new Array(...timingSet).map((item: any) => JSON.parse(item)) - const timingsAll = _.chain(timingsPayloadArr) - .filter((payload) => _.some(payload.list, { code: 'type', value: 'All' })) - .value() - - // Getting timings object for 'Delivery', 'Self-Pickup' and 'Order' - const timingsOther = _.chain(timingsPayloadArr) - .filter( - (payload) => - _.some(payload.list, { code: 'type', value: 'Order' }) || - _.some(payload.list, { code: 'type', value: 'Delivery' }) || - _.some(payload.list, { code: 'type', value: 'Self-Pickup' }), - ) - .value() + if (isEmpty(timingSet)) { + const key = `prvdr${i}tags/timing` + businessErrors[key] = `timing construct is mandatory in /bpp/providers[${i}]/tags` + } else { + const timingsPayloadArr = new Array(...timingSet).map((item: any) => JSON.parse(item)) + const timingsAll = _.chain(timingsPayloadArr) + .filter((payload) => _.some(payload.list, { code: 'type', value: 'All' })) + .value() + + // Getting timings object for 'Delivery', 'Self-Pickup' and 'Order' + const timingsOther = _.chain(timingsPayloadArr) + .filter( + (payload) => + _.some(payload.list, { code: 'type', value: 'Order' }) || + _.some(payload.list, { code: 'type', value: 'Delivery' }) || + _.some(payload.list, { code: 'type', value: 'Self-Pickup' }), + ) + .value() + + if (timingsAll.length > 0 && timingsOther.length > 0) { + businessErrors[`prvdr${i}tags/timing`] = + `If the timings of type 'All' is provided then timings construct for 'Order'/'Delivery'/'Self-Pickup' is not required` + } - if (timingsAll.length > 0 && timingsOther.length > 0) { - errorObj[`prvdr${i}tags/timing`] = - `If the timings of type 'All' is provided then timings construct for 'Order'/'Delivery'/'Self-Pickup' is not required` - } + const arrTimingTypes = new Set() + + function checkTimingTag(tag: any) { + const typeObject = tag.list.find((item: { code: string }) => item.code === 'type') + const typeValue = typeObject ? typeObject.value : null + arrTimingTypes.add(typeValue) + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + businessErrors[`prvdr${i}/day_to$/${typeValue}`] = `Invalid value for '${item.code}': ${item.value}` + } - const arrTimingTypes = new Set() - - function checkTimingTag(tag: any) { - const typeObject = tag.list.find((item: { code: string }) => item.code === 'type') - const typeValue = typeObject ? typeObject.value : null - arrTimingTypes.add(typeValue) - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj[`prvdr${i}/day_to$/${typeValue}`] = `Invalid value for '${item.code}': ${item.value}` + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + businessErrors[`prvdr${i}/tags/time_to/${typeValue}`] = + `Invalid time format for '${item.code}': ${item.value}` + } + break + case 'location': + if (!prvdrLocationIds.has(item.value)) { + businessErrors[`prvdr${i}/tags/location/${typeValue}`] = + `Invalid location value as it's unavailable in location/ids` + } + break + case 'type': + break + default: + businessErrors[`prvdr${i}/tags/tag_timings/${typeValue}`] = `Invalid list.code for 'timing': ${item.code}` } + } + + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - errorObj[`prvdr${i}/tags/time_to/${typeValue}`] = - `Invalid time format for '${item.code}': ${item.value}` + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) + + if (dayTo < dayFrom) { + businessErrors[`prvdr${i}/tags/day_from/${typeValue}`] = + "'day_to' must be greater than or equal to 'day_from'" } - break - case 'location': - if (!prvdrLocationIds.has(item.value)) { - errorObj[`prvdr${i}/tags/location/${typeValue}`] = - `Invalid location value as it's unavailable in location/ids` + + if (timeTo <= timeFrom) { + businessErrors[`prvdr${i}/tags/time_from/${typeValue}`] = "'time_to' must be greater than 'time_from'" } - break - case 'type': - break - default: - errorObj[`prvdr${i}/tags/tag_timings/${typeValue}`] = `Invalid list.code for 'timing': ${item.code}` + } } - } - - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') - - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) - if (dayTo < dayFrom) { - errorObj[`prvdr${i}/tags/day_from/${typeValue}`] = - "'day_to' must be greater than or equal to 'day_from'" + if (timingsAll.length > 0) { + if (timingsAll.length > 1) { + businessErrors[`prvdr${i}tags/timing/All`] = `The timings object for 'All' should be provided once!` + } + checkTimingTag(timingsAll[0]) } - if (timeTo <= timeFrom) { - errorObj[`prvdr${i}/tags/time_from/${typeValue}`] = "'time_to' must be greater than 'time_from'" + if (timingsOther.length > 0) { + timingsOther.forEach((tagTimings: any) => { + checkTimingTag(tagTimings) + }) + onSearchFFTypeSet.forEach((type: any) => { + if (!arrTimingTypes.has(type)) { + businessErrors[`prvdr${i}/tags/timing/${type}`] = `The timings object must be present for ${type} in the tags` + } + arrTimingTypes.forEach((type: any) => { + if (type != 'Order' && type != 'All' && !onSearchFFTypeSet.has(type)) { + businessErrors[`prvdr${i}/tags/timing/${type}`] = + `The timings object ${type} is not present in the onSearch fulfillments` + } + }) + if (!arrTimingTypes.has('Order')) { + businessErrors[`prvdr${i}/tags/timing/order`] = `The timings object must be present for Order in the tags` + } + }) } } + } catch (error: any) { + logger.error( + `!!Error while checking serviceability and timing construct for bpp/providers[${i}], ${error.stack}`, + ) } - if (timingsAll.length > 0) { - if (timingsAll.length > 1) { - errorObj[`prvdr${i}tags/timing/All`] = `The timings object for 'All' should be provided once!` - } - checkTimingTag(timingsAll[0]) - } + try { + logger.info( + `Checking if catalog_link type in message/catalog/bpp/providers[${i}]/tags[1]/list[0] is link or inline`, + ) + const tags = bppPrvdrs[i].tags - if (timingsOther.length > 0) { - timingsOther.forEach((tagTimings: any) => { - checkTimingTag(tagTimings) - }) - onSearchFFTypeSet.forEach((type: any) => { - if (!arrTimingTypes.has(type)) { - errorObj[`prvdr${i}/tags/timing/${type}`] = `The timings object must be present for ${type} in the tags` + let list: any = [] + tags.map((data: any) => { + if (data.code == 'catalog_link') { + list = data.list } - arrTimingTypes.forEach((type: any) => { - if (type != 'Order' && type != 'All' && !onSearchFFTypeSet.has(type)) { - errorObj[`prvdr${i}/tags/timing/${type}`] = - `The timings object ${type} is not present in the onSearch fulfillments` + }) + + list.map((data: any) => { + if (data.code === 'type') { + if (data.value === 'link') { + if (bppPrvdrs[0].items) { + businessErrors[`message/catalog/bpp/providers[0]`] = + `Items arrays should not be present in message/catalog/bpp/providers[${i}]` + } } - }) - if (!arrTimingTypes.has('Order')) { - errorObj[`prvdr${i}/tags/timing/order`] = `The timings object must be present for Order in the tags` } }) + } catch (error: any) { + logger.error(`Error while checking the type of catalog_link`) } + + i++ } - } catch (error: any) { - logger.error( - `!!Error while checking serviceability and timing construct for bpp/providers[${i}], ${error.stack}`, - ) - } - try { - logger.info( - `Checking if catalog_link type in message/catalog/bpp/providers[${i}]/tags[1]/list[0] is link or inline`, - ) - const tags = bppPrvdrs[i].tags + try { + if ( + onSearchCatalog['bpp/providers'] && + onSearchCatalog['bpp/providers'][i] && + Array.isArray(onSearchCatalog['bpp/providers'][i]['fulfillments']) + ) { + const fulfillments: any = onSearchCatalog['bpp/providers'][i]['fulfillments'] + fulfillments.forEach((fulfillment: any) => { + const hasSelfPickupFulfillment = fulfillments.some((f: any) => f.type === 'Self-Pickup') + + const selfPickupTag = fulfillment.tags?.find( + (tag: any) => + tag.code === 'timing' && + tag.list?.some((item: any) => item.code === 'type' && item.value === 'Self-Pickup'), + ) - let list: any = [] - tags.map((data: any) => { - if (data.code == 'catalog_link') { - list = data.list - } - }) + if (flow === '002') { + // Flow is 002 => Self-Pickup fulfillment is required + if (!hasSelfPickupFulfillment) { + const key = `prvdr${i}fulfillment_self_pickup_required` + businessErrors[key] = `Provider[${i}] with flow=002 must have at least one fulfillment of type 'Self-Pickup'` + } + } else { + // For all other flows => Self-Pickup timing tag is required and must be valid + if (!selfPickupTag) { + const key = `prvdr${i}tag_self_pickup_required` + businessErrors[key] = `Provider[${i}] with flow≠002 must have a 'timing' tag with list.type='Self-Pickup'` + } else { + // const timingKeys = ['day_from', 'day_to', 'time_from', 'time_to'] + const tagListMap = Object.fromEntries(selfPickupTag.list.map((t: any) => [t.code, t.value])) + const locationId = tagListMap['location'] + + if (locationId) { + if (!prvdrLocId.has(locationId)) { + const key = `prvdr${i}tag_timing_invalid_location` + businessErrors[key] = + `'location' in Self-Pickup timing tag must match one of the provider[${i}]'s location ids` + } + } else { + const key = `prvdr${i}tag_timing_missing_location` + businessErrors[key] = `'location' is missing in Self-Pickup timing tag for provider[${i}]` + } + + // Validate day_from/to are between 1 and 7 + ;['day_from', 'day_to'].forEach((code) => { + const val = parseInt(tagListMap[code]) + if (isNaN(val) || val < 1 || val > 7) { + const key = `prvdr${i}tag_timing_invalid_${code}` + businessErrors[key] = `Invalid value for ${code}: ${tagListMap[code]} in Self-Pickup timing tag` + } + }) - list.map((data: any) => { - if (data.code === 'type') { - if (data.value === 'link') { - if (bppPrvdrs[0].items) { - errorObj[`message/catalog/bpp/providers[0]`] = - `Items arrays should not be present in message/catalog/bpp/providers[${i}]` + // Validate time_from/to format + ;['time_from', 'time_to'].forEach((code) => { + const val = tagListMap[code] + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(val)) { + const key = `prvdr${i}tag_timing_invalid_${code}` + businessErrors[key] = `Invalid format for ${code}: ${val} in Self-Pickup timing tag (expected HHMM)` + } + }) + + // Additional validation: time_to > time_from + const tFrom = parseInt(tagListMap['time_from']) + const tTo = parseInt(tagListMap['time_to']) + if (!isNaN(tFrom) && !isNaN(tTo) && tTo <= tFrom) { + const key = `prvdr${i}tag_timing_time_order` + businessErrors[key] = `'time_to' (${tTo}) must be greater than 'time_from' (${tFrom}) for Self-Pickup` + } + } } - } + }) } - }) + else { + logger.error(`Provider or fulfillments missing for bpp/providers[${i}]`) + } + } catch (error: any) { + logger.error(`!!Errors while checking fulfillments in bpp/providers[${i}], ${error.stack}`) + } + + setValue('onSearchItems', itemsArray) + setValue(`${ApiSequence.ON_SEARCH}prvdrsId`, prvdrsId) + setValue(`${ApiSequence.ON_SEARCH}prvdrLocId`, prvdrLocId) + setValue(`${ApiSequence.ON_SEARCH}itemsId`, itemsId) } catch (error: any) { - logger.error(`Error while checking the type of catalog_link`) + logger.error(`!!Error while checking Providers info in /${constants.ON_SEARCH}, ${error.stack}`) } + try { + logger.info(`Checking for errors in default flow in /${constants.ON_SEARCH}`) + const providers = data.message.catalog['bpp/providers'] - i++ - } - - try { - const fulfillments: any = onSearchCatalog['bpp/providers'][i]['fulfillments'] - fulfillments.forEach((fulfillment: any) => { - const hasSelfPickupFulfillment = fulfillments.some((f: any) => f.type === 'Self-Pickup') + providers.forEach((provider: any) => { + let customGroupDetails: any = {} - const selfPickupTag = fulfillment.tags?.find( - (tag: any) => - tag.code === 'timing' && - tag.list?.some((item: any) => item.code === 'type' && item.value === 'Self-Pickup'), - ) + provider?.categories.forEach((category: any) => { + const id: string = category?.id + const customGroupTag = category.tags.find( + (tag: any) => tag.code === 'type' && tag.list.some((item: any) => item.value === 'custom_group'), + ) - if (flow === '002') { - // Flow is 002 => Self-Pickup fulfillment is required - if (!hasSelfPickupFulfillment) { - const key = `prvdr${i}fulfillment_self_pickup_required` - errorObj[key] = `Provider[${i}] with flow=002 must have at least one fulfillment of type 'Self-Pickup'` - } - } else { - // For all other flows => Self-Pickup timing tag is required and must be valid - if (!selfPickupTag) { - const key = `prvdr${i}tag_self_pickup_required` - errorObj[key] = `Provider[${i}] with flow≠002 must have a 'timing' tag with list.type='Self-Pickup'` - } else { - // const timingKeys = ['day_from', 'day_to', 'time_from', 'time_to'] - const tagListMap = Object.fromEntries(selfPickupTag.list.map((t: any) => [t.code, t.value])) - const locationId = tagListMap['location'] - - if (locationId) { - if (!prvdrLocId.has(locationId)) { - const key = `prvdr${i}tag_timing_invalid_location` - errorObj[key] = - `'location' in Self-Pickup timing tag must match one of the provider[${i}]'s location ids` - } - } else { - const key = `prvdr${i}tag_timing_missing_location` - errorObj[key] = `'location' is missing in Self-Pickup timing tag for provider[${i}]` - } + if (customGroupTag) { + const configTag = category.tags.find((tag: any) => tag.code === 'config') + const min = configTag ? parseInt(configTag.list.find((item: any) => item.code === 'min')?.value, 10) : 0 + const max = configTag ? parseInt(configTag.list.find((item: any) => item.code === 'max')?.value, 10) : 0 - // Validate day_from/to are between 1 and 7 - ;['day_from', 'day_to'].forEach((code) => { - const val = parseInt(tagListMap[code]) - if (isNaN(val) || val < 1 || val > 7) { - const key = `prvdr${i}tag_timing_invalid_${code}` - errorObj[key] = `Invalid value for ${code}: ${tagListMap[code]} in Self-Pickup timing tag` + if (min > max) { + businessErrors[`${provider.id}/categories/${id}`] = `The "min" is more than "max"` } - }) - - // Validate time_from/to format - ;['time_from', 'time_to'].forEach((code) => { - const val = tagListMap[code] - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(val)) { - const key = `prvdr${i}tag_timing_invalid_${code}` - errorObj[key] = `Invalid format for ${code}: ${val} in Self-Pickup timing tag (expected HHMM)` + customGroupDetails[id] = { + min: min, + max: max, + numberOfDefaults: 0, + numberOfElements: 0, } - }) - - // Additional validation: time_to > time_from - const tFrom = parseInt(tagListMap['time_from']) - const tTo = parseInt(tagListMap['time_to']) - if (!isNaN(tFrom) && !isNaN(tTo) && tTo <= tFrom) { - const key = `prvdr${i}tag_timing_time_order` - errorObj[key] = `'time_to' (${tTo}) must be greater than 'time_from' (${tFrom}) for Self-Pickup` } - } - } - }) - } catch (error: any) { - logger.error(`!!Errors while checking fulfillments in bpp/providers[${i}], ${error.stack}`) - } - - setValue('onSearchItems', itemsArray) - setValue(`${ApiSequence.ON_SEARCH}prvdrsId`, prvdrsId) - setValue(`${ApiSequence.ON_SEARCH}prvdrLocId`, prvdrLocId) - setValue(`${ApiSequence.ON_SEARCH}itemsId`, itemsId) - } catch (error: any) { - logger.error(`!!Error while checking Providers info in /${constants.ON_SEARCH}, ${error.stack}`) - } - try { - logger.info(`Checking for errors in default flow in /${constants.ON_SEARCH}`) - const providers = data.message.catalog['bpp/providers'] - - providers.forEach((provider: any) => { - let customGroupDetails: any = {} + }) - provider?.categories.forEach((category: any) => { - const id: string = category?.id - const customGroupTag = category.tags.find( - (tag: any) => tag.code === 'type' && tag.list.some((item: any) => item.value === 'custom_group'), - ) + let combinedIds: any = [] - if (customGroupTag) { - const configTag = category.tags.find((tag: any) => tag.code === 'config') - const min = configTag ? parseInt(configTag.list.find((item: any) => item.code === 'min')?.value, 10) : 0 - const max = configTag ? parseInt(configTag.list.find((item: any) => item.code === 'max')?.value, 10) : 0 + provider?.items.forEach((item: any) => { + const typeTag = item.tags.find((tag: any) => tag.code === 'type') + const typeValue = typeTag ? typeTag.list.find((listItem: any) => listItem.code === 'type')?.value : null - if (min > max) { - errorObj[`${provider.id}/categories/${id}`] = `The "min" is more than "max"` - } - customGroupDetails[id] = { - min: min, - max: max, - numberOfDefaults: 0, - numberOfElements: 0, - } - } - }) + if (typeValue === 'item') { + const customGroupTags = item.tags.filter((tag: any) => tag.code === 'custom_group') + combinedIds = customGroupTags.flatMap((tag: any) => tag.list.map((listItem: any) => listItem.value)) + } else if (typeValue === 'customization') { + const parentTag = item.tags.find((tag: any) => tag.code === 'parent') + const customizationGroupId = parentTag?.list.find((listItem: any) => listItem.code === 'id')?.value - let combinedIds: any = [] + if (customizationGroupId && customGroupDetails[customizationGroupId]) { + customGroupDetails[customizationGroupId].numberOfElements += 1 - provider?.items.forEach((item: any) => { - const typeTag = item.tags.find((tag: any) => tag.code === 'type') - const typeValue = typeTag ? typeTag.list.find((listItem: any) => listItem.code === 'type')?.value : null + const defaultParent = parentTag?.list.find( + (listItem: any) => listItem.code === 'default' && listItem.value === 'yes', + ) + if (defaultParent) { + customGroupDetails[customizationGroupId].numberOfDefaults += 1 - if (typeValue === 'item') { - const customGroupTags = item.tags.filter((tag: any) => tag.code === 'custom_group') - combinedIds = customGroupTags.flatMap((tag: any) => tag.list.map((listItem: any) => listItem.value)) - } else if (typeValue === 'customization') { - const parentTag = item.tags.find((tag: any) => tag.code === 'parent') - const customizationGroupId = parentTag?.list.find((listItem: any) => listItem.code === 'id')?.value + const childTag = item.tags.find((tag: any) => tag.code === 'child') + if (childTag) { + const childIds = childTag.list.map((listItem: any) => listItem.value) + combinedIds = [...combinedIds, ...childIds] + } + } + } + } + }) - if (customizationGroupId && customGroupDetails[customizationGroupId]) { - customGroupDetails[customizationGroupId].numberOfElements += 1 + combinedIds.forEach((id: any) => { + if (customGroupDetails[id]) { + const group = customGroupDetails[id] + const min = group.min + const max = group.max - const defaultParent = parentTag?.list.find( - (listItem: any) => listItem.code === 'default' && listItem.value === 'yes', - ) - if (defaultParent) { - customGroupDetails[customizationGroupId].numberOfDefaults += 1 + if (group.numberOfElements <= max) { + businessErrors[`${provider.id}/categories/${id}/number_of_elements`] = + 'The number of elements in this customization group is less than the maximum that can be selected.' + } - const childTag = item.tags.find((tag: any) => tag.code === 'child') - if (childTag) { - const childIds = childTag.list.map((listItem: any) => listItem.value) - combinedIds = [...combinedIds, ...childIds] + if (min > 0 && group.numberOfDefaults < min) { + businessErrors[`${provider.id}/categories/${id}/number_of_defaults`] = + 'The number of defaults of this customization group is less than the minimum that can be selected.' } } - } - } - }) + }) - combinedIds.forEach((id: any) => { - if (customGroupDetails[id]) { - const group = customGroupDetails[id] - const min = group.min - const max = group.max + const customGroupIds = Object.keys(customGroupDetails) + customGroupIds.forEach((id) => { + const group = customGroupDetails[id] + const max = group.max - if (group.numberOfElements <= max) { - errorObj[`${provider.id}/categories/${id}/number_of_elements`] = - 'The number of elements in this customization group is less than the maximum that can be selected.' - } + if (group.numberOfElements < max) { + businessErrors[`${provider.id}/categories/${id}/number_of_defaults`] = + 'The number of elements in this customization group is less than the maximum that can be selected.' + } + }) + }) + } catch (error: any) { + logger.error(`Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`) + } - if (min > 0 && group.numberOfDefaults < min) { - errorObj[`${provider.id}/categories/${id}/number_of_defaults`] = - 'The number of defaults of this customization group is less than the minimum that can be selected.' - } - } - }) - const customGroupIds = Object.keys(customGroupDetails) - customGroupIds.forEach((id) => { - const group = customGroupDetails[id] - const max = group.max - if (group.numberOfElements < max) { - errorObj[`${provider.id}/categories/${id}/number_of_defaults`] = - 'The number of elements in this customization group is less than the maximum that can be selected.' - } - }) - }) - } catch (error: any) { - logger.error(`Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`) - } + //Validating Offers + try { + logger.info(`Checking offers under bpp/providers`) - + // Iterate through bpp/providers + for (let i in onSearchCatalog['bpp/providers']) { + const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? [] + if (!offers || !Array.isArray(offers)) { + const key = `bpp/providers[${i}]/offers` + businessErrors[key] = `Offers must be an array in bpp/providers[${i}]` + continue + } - //Validating Offers - try { - logger.info(`Checking offers under bpp/providers`) - - // Iterate through bpp/providers - for (let i in onSearchCatalog['bpp/providers']) { - const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? [] - if (!offers || !Array.isArray(offers)) { - const key = `bpp/providers[${i}]/offers` - errorObj[key] = `Offers must be an array in bpp/providers[${i}]` - continue - } + offers.forEach((offer: any, offerIndex: number) => { + // Validate mandatory fields + if (!offer.id) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/id` + businessErrors[key] = `Offer ID is mandatory for offers[${offerIndex}]` + logger.error(`Offer ID is mandatory for offers[${offerIndex}]`) + } - offers.forEach((offer: any, offerIndex: number) => { - // Validate mandatory fields - if (!offer.id) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/id` - errorObj[key] = `Offer ID is mandatory for offers[${offerIndex}]` - logger.error(`Offer ID is mandatory for offers[${offerIndex}]`) - } + if (!offer.descriptor || !offer.descriptor.code) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/descriptor` + businessErrors[key] = `Descriptor with code is mandatory for offers[${offerIndex}]` + logger.error(`Descriptor with code is mandatory for offers[${offerIndex}]`) + } - if (!offer.descriptor || !offer.descriptor.code) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/descriptor` - errorObj[key] = `Descriptor with code is mandatory for offers[${offerIndex}]` - logger.error(`Descriptor with code is mandatory for offers[${offerIndex}]`) - } + if (!offer.location_ids || !Array.isArray(offer.location_ids) || offer.location_ids.length === 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/location_ids` + businessErrors[key] = `Location IDs array is mandatory for offers[${offerIndex}]` + logger.error(`Location IDs array is mandatory for offers[${offerIndex}]`) + } - if (!offer.location_ids || !Array.isArray(offer.location_ids) || offer.location_ids.length === 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/location_ids` - errorObj[key] = `Location IDs array is mandatory for offers[${offerIndex}]` - logger.error(`Location IDs array is mandatory for offers[${offerIndex}]`) - } + if (!offer.item_ids || !Array.isArray(offer.item_ids) || offer.item_ids.length === 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/item_ids` + businessErrors[key] = `Item IDs array is mandatory for offers[${offerIndex}]` + logger.error(`Item IDs array is mandatory for offers[${offerIndex}]`) + } - if (!offer.item_ids || !Array.isArray(offer.item_ids) || offer.item_ids.length === 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/item_ids` - errorObj[key] = `Item IDs array is mandatory for offers[${offerIndex}]` - logger.error(`Item IDs array is mandatory for offers[${offerIndex}]`) - } + if (!offer.time || !offer.time.label || !offer.time.range || !offer.time.range.start || !offer.time.range.end) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/time` + businessErrors[key] = `Time object with label and range (start/end) is mandatory for offers[${offerIndex}]` + logger.error(`Time object with label and range (start/end) is mandatory for offers[${offerIndex}]`) + } - if (!offer.time || !offer.time.label || !offer.time.range || !offer.time.range.start || !offer.time.range.end) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/time` - errorObj[key] = `Time object with label and range (start/end) is mandatory for offers[${offerIndex}]` - logger.error(`Time object with label and range (start/end) is mandatory for offers[${offerIndex}]`) - } + const tags = offer.tags + if (!tags || !Array.isArray(tags)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` + businessErrors[key] = + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` + logger.error( + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, + ) + return + } - const tags = offer.tags - if (!tags || !Array.isArray(tags)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` - errorObj[key] = - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` - logger.error( - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, - ) - return - } + // Validate based on offer type + switch (offer.descriptor?.code) { + case 'discount': + // Validate 'qualifier' + const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - // Validate based on offer type - switch (offer.descriptor?.code) { - case 'discount': - // Validate 'qualifier' - const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + // Validate 'benefit' + const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') + if (!benefitDiscount) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag is required for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag is required for offers[${offerIndex}]`) + } else { + const valueTypeItem = benefitDiscount.list.find((item: any) => item.code === 'value_type') + const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value') + const valueCapItem = benefitDiscount.list.find((item: any) => item.code === 'value_cap') + + if (!valueTypeItem) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_type` + businessErrors[key] = `'value_type' is required in benefit tag for offers[${offerIndex}]` + logger.error(`'value_type' is required in benefit tag for offers[${offerIndex}]`) + } - // Validate 'benefit' - const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') - if (!benefitDiscount) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag is required for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag is required for offers[${offerIndex}]`) - } else { - const valueTypeItem = benefitDiscount.list.find((item: any) => item.code === 'value_type') - const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value') - const valueCapItem = benefitDiscount.list.find((item: any) => item.code === 'value_cap') - - if (!valueTypeItem) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_type` - errorObj[key] = `'value_type' is required in benefit tag for offers[${offerIndex}]` - logger.error(`'value_type' is required in benefit tag for offers[${offerIndex}]`) - } + if (!valueItem) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' is required in benefit tag for offers[${offerIndex}]` + logger.error(`'value' is required in benefit tag for offers[${offerIndex}]`) + } else { + // Validate value is a proper number + const value = valueItem.value + if (isNaN(parseFloat(value)) || !/^-?\d+(\.\d+)?$/.test(value)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' in benefit tag must be a valid number for offers[${offerIndex}]` + logger.error(`'value' in benefit tag must be a valid number for offers[${offerIndex}]`) + } else if (parseFloat(value) >= 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]` + logger.error(`'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]`) + } + } - if (!valueItem) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' is required in benefit tag for offers[${offerIndex}]` - logger.error(`'value' is required in benefit tag for offers[${offerIndex}]`) - } else { - // Validate value is a proper number - const value = valueItem.value - if (isNaN(parseFloat(value)) || !/^-?\d+(\.\d+)?$/.test(value)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' in benefit tag must be a valid number for offers[${offerIndex}]` - logger.error(`'value' in benefit tag must be a valid number for offers[${offerIndex}]`) - } else if (parseFloat(value) >= 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]` - logger.error(`'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]`) + // Validate value_cap if present + if (valueCapItem) { + const valueCap = valueCapItem.value + if (isNaN(parseFloat(valueCap)) || !/^-?\d+(\.\d+)?$/.test(valueCap)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_cap` + businessErrors[key] = `'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]` + logger.error(`'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]`) + } + } + } + break + + case 'buyXgetY': + // Validate 'qualifier' + const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierBuyXgetY || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`) } - } - // Validate value_cap if present - if (valueCapItem) { - const valueCap = valueCapItem.value - if (isNaN(parseFloat(valueCap)) || !/^-?\d+(\.\d+)?$/.test(valueCap)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_cap` - errorObj[key] = `'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]` - logger.error(`'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]`) + // Validate 'benefit' + const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') + if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) + } + break + + case 'freebie': + // Validate 'qualifier' + const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) } - } - } - break - - case 'buyXgetY': - // Validate 'qualifier' - const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierBuyXgetY || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`) - } - // Validate 'benefit' - const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') - if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) - } - break - - case 'freebie': - // Validate 'qualifier' - const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + // Validate 'benefit' + const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitFreebie || + !benefitFreebie.list.some((item: any) => item.code === 'item_count') || + !benefitFreebie.list.some((item: any) => item.code === 'item_id') || + !benefitFreebie.list.some((item: any) => item.code === 'item_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, + ) + } + break + + case 'slab': + // Validate 'qualifier' + const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - // Validate 'benefit' - const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitFreebie || - !benefitFreebie.list.some((item: any) => item.code === 'item_count') || - !benefitFreebie.list.some((item: any) => item.code === 'item_id') || - !benefitFreebie.list.some((item: any) => item.code === 'item_value') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, - ) - } - break - - case 'slab': - // Validate 'qualifier' - const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + // Validate 'benefit' + const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitSlab || + !benefitSlab.list.some((item: any) => item.code === 'value') || + !benefitSlab.list.some((item: any) => item.code === 'value_type') || + !benefitSlab.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) + } + break + + case 'combo': + // Validate 'qualifier' + const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierCombo || + !qualifierCombo.list.some((item: any) => item.code === 'min_value') || + !qualifierCombo.list.some((item: any) => item.code === 'item_id') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) + } - // Validate 'benefit' - const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitSlab || - !benefitSlab.list.some((item: any) => item.code === 'value') || - !benefitSlab.list.some((item: any) => item.code === 'value_type') || - !benefitSlab.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) - } - break - - case 'combo': - // Validate 'qualifier' - const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierCombo || - !qualifierCombo.list.some((item: any) => item.code === 'min_value') || - !qualifierCombo.list.some((item: any) => item.code === 'item_id') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) - } + // Validate 'benefit' + const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitCombo || + !benefitCombo.list.some((item: any) => item.code === 'value') || + !benefitCombo.list.some((item: any) => item.code === 'value_type') || + !benefitCombo.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) + } + break + + case 'delivery': + // Validate 'qualifier' + const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - // Validate 'benefit' - const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitCombo || - !benefitCombo.list.some((item: any) => item.code === 'value') || - !benefitCombo.list.some((item: any) => item.code === 'value_type') || - !benefitCombo.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) - } - break - - case 'delivery': - // Validate 'qualifier' - const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + // Validate 'benefit' + const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitDelivery || + !benefitDelivery.list.some((item: any) => item.code === 'value') || + !benefitDelivery.list.some((item: any) => item.code === 'value_type') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}]`) + } + break + + case 'exchange': + // Validate 'qualifier' + const qualifierExchange = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierExchange || !qualifierExchange.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - // Validate 'benefit' - const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitDelivery || - !benefitDelivery.list.some((item: any) => item.code === 'value') || - !benefitDelivery.list.some((item: any) => item.code === 'value_type') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}]`) - } - break - - case 'exchange': - // Validate 'qualifier' - const qualifierExchange = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierExchange || !qualifierExchange.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } + // Validate that benefits should not exist or should be empty + const benefitExchange = tags.find((tag: any) => tag.code === 'benefit') + if (benefitExchange && benefitExchange.list.length > 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`) + } + break - // Validate that benefits should not exist or should be empty - const benefitExchange = tags.find((tag: any) => tag.code === 'benefit') - if (benefitExchange && benefitExchange.list.length > 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`) + default: + logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) } - break - - default: - logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) + }) } - }) + } catch (error: any) { + logger.error(`Error while checking offers under bpp/providers: ${error.stack}`) + } } + + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors } + } catch (error: any) { - logger.error(`Error while checking offers under bpp/providers: ${error.stack}`) + logger.error( + `Error while checking for JSON structure and required fields for ${ApiSequence.ON_SEARCH} API with schemaValidation: ${schemaValidation}`, + ) + return { + error: 'error while checking on_search api', + } } - - return Object.keys(errorObj).length > 0 && errorObj } diff --git a/utils/Retail_.1.2.5/Search/on_search.ts b/utils/Retail_.1.2.5/Search/on_search.ts index 3d4c5451..e5b84359 100644 --- a/utils/Retail_.1.2.5/Search/on_search.ts +++ b/utils/Retail_.1.2.5/Search/on_search.ts @@ -33,2222 +33,2317 @@ import { applianceData } from '../../../constants/appliance' import { fashion } from '../../../constants/fashion' import { DOMAIN, FLOW, OFFERSFLOW, statutory_reqs } from '../../enum' import { ret1aJSON } from '../../../constants/ret1a' -export const checkOnsearch = (data: any,flow?: string) => { - console.log('in this on_search 1.2.5') - - if (!data || isObjectEmpty(data)) { - return { [ApiSequence.ON_SEARCH]: 'JSON cannot be empty' } - } - const { message, context } = data - - if (!message || !context || !message.catalog || isObjectEmpty(message) || isObjectEmpty(message.catalog)) { - return { missingFields: '/context, /message, /catalog or /message/catalog is missing or empty' } - } - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_SEARCH, data) - let collect_payment_tags: any = {} - - setValue(`${ApiSequence.ON_SEARCH}_context`, context) - setValue(`${ApiSequence.ON_SEARCH}_message`, message) - const providerOffers: any[] = message?.catalog['bpp/providers']?.flatMap((provider: any) => provider?.offers || []) - if (providerOffers && providerOffers.length > 0) { - setValue(`${ApiSequence.ON_SEARCH}_offers`, providerOffers) - } +export const checkOnsearch = (data: any, flow?: string, stateless: boolean = false, schemaValidation?: boolean) => { + console.log('in this on_search 1.2.5') let errorObj: any = {} + const schemaErrors: Record = {} + const businessErrors: Record = {} - if (schemaValidation !== 'error') { - Object.assign(errorObj, schemaValidation) - } - - validateBapUri(context.bap_uri, context.bap_id, errorObj) - validateBppUri(context.bpp_uri, context.bpp_id, errorObj) - if (context.transaction_id == context.message_id) { - errorObj['on_search_full_catalog_refresh'] = - `Context transaction_id (${context.transaction_id}) and message_id (${context.message_id}) can't be the same.` - } try { - logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(getValue(`${ApiSequence.SEARCH}_msgId`), context.message_id)) { - errorObj[`${ApiSequence.ON_SEARCH}_msgId`] = - `Message Ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` - } - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_SEARCH}, ${error.stack}`) - } - - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } + logger.info(`Checking JSON structure and required fields for ${ApiSequence.ON_SEARCH} API with schemaValidation: ${schemaValidation}`) - const checkBap = checkBppIdOrBapId(context.bap_id) - const checkBpp = checkBppIdOrBapId(context.bpp_id) - - if (checkBap) Object.assign(errorObj, { bap_id: 'context/bap_id should not be a url' }) - if (checkBpp) Object.assign(errorObj, { bpp_id: 'context/bpp_id should not be a url' }) - - try { - logger.info(`Checking for context in /${constants.ON_SEARCH}`) - const contextRes: any = checkContext(context, constants.ON_SEARCH) - if (!contextRes?.valid) { - Object.assign(errorObj, contextRes.ERRORS) + if (!data || isObjectEmpty(data)) { + errorObj[ApiSequence.ON_SEARCH] = 'JSON cannot be empty' + return errorObj } - } catch (error: any) { - logger.error(`Error while checking for context in /${constants.ON_SEARCH}, ${error.stack}`) - } - - setValue(`${ApiSequence.ON_SEARCH}`, data) - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - try { - logger.info(`Storing BAP_ID and BPP_ID in /${constants.ON_SEARCH}`) - setValue('bapId', context.bap_id) - setValue('bppId', context.bpp_id) - } catch (error: any) { - logger.error(`!!Error while storing BAP and BPP Ids in /${constants.ON_SEARCH}, ${error.stack}`) - } - - try { - logger.info(`Comparing transaction Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(searchContext.transaction_id, context.transaction_id)) { - errorObj.transaction_id = `Transaction Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + const { message, context } = data + if (!message || !context || !message.catalog || isObjectEmpty(message) || isObjectEmpty(message.catalog)) { + errorObj['missingFields'] = '/context, /message, /catalog or /message/catalog is missing or empty' + return Object.keys(errorObj).length > 0 && errorObj } - } catch (error: any) { - logger.info( - `Error while comparing transaction ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } - - // removed timestamp difference check - // try { - // logger.info(`Comparing timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - // const tmpstmp = searchContext?.timestamp - // if (_.gte(tmpstmp, context.timestamp)) { - // errorObj.tmpstmp = `Timestamp for /${constants.SEARCH} api cannot be greater than or equal to /${constants.ON_SEARCH} api` - // } else { - // const timeDiff = timeDifference(context.timestamp, tmpstmp) - // logger.info(timeDiff) - // if (timeDiff > 5000) { - // errorObj.tmpstmp = `context/timestamp difference between /${constants.ON_SEARCH} and /${constants.SEARCH} should be less than 5 sec` - // } - // } - // } catch (error: any) { - // logger.info( - // `Error while comparing timestamp for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - // ) - // } - try { - logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) - if (!_.isEqual(searchContext.message_id, context.message_id)) { - errorObj.message_id = `Message Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + // Schema validation - run unless explicitly disabled + if (schemaValidation !== false) { + const schemaDomain = context.domain.split(':')[1] + logger.info(`checkOnsearch: calling schema validation with domain=${schemaDomain}, api=${constants.ON_SEARCH}`) + const schemaValidation = validateSchemaRetailV2(schemaDomain, constants.ON_SEARCH, data) + logger.info(`checkOnsearch: schema validation result:`, schemaValidation) + + logger.info(`checkOnsearch: checking if schema validation !== 'error': ${schemaValidation !== 'error'}`) + if (schemaValidation !== 'error') { + logger.info(`checkOnsearch: adding schema errors to schemaErrors:`, schemaValidation) + Object.assign(schemaErrors, schemaValidation) + } else { + logger.info(`checkOnsearch: schema validation passed, no schema errors to add`) + } } - } catch (error: any) { - logger.info( - `Error while comparing message ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } - try { - const providers = data.message.catalog['bpp/providers'] - providers.forEach((provider: any) => { - const items = provider.items.forEach((item: any) => { - console.log('item inside items', item?.['@ondc/org/available_on_cod']) - }) - - console.log('items>>', items) - console.log('provider items', provider.items) - const address = provider.locations[0].address - - if (address) { - const area_code = Number.parseInt(address.area_code) - const std = context.city.split(':')[1] - - logger.info(`Comparing area_code and STD Code for /${constants.ON_SEARCH}`) - const areaWithSTD = compareSTDwithArea(area_code, std) - if (!areaWithSTD) { - logger.error(`STD code does not match with correct area_code in /${constants.ON_SEARCH}`) - errorObj.invldAreaCode = `STD code does not match with correct area_code in /${constants.ON_SEARCH}` - } - } - }) - } catch (error: any) { - logger.error( - `Error while matching area_code and std code for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, - ) - } + // Business logic validation - run unless explicitly schema-only + if (schemaValidation !== true) { + let collect_payment_tags: any = {} - const onSearchCatalog: any = message.catalog - const onSearchFFIdsArray: any = [] - const prvdrsId = new Set() - const prvdrLocId = new Set() - const itemsId = new Set() - const parentItemIdSet = new Set() - const onSearchFFTypeSet = new Set() - const itemsArray: any = [] - let itemIdList: any = [] - setValue('tmpstmp', context.timestamp) - - // Storing static fulfillment ids in onSearchFFIdsArray, OnSearchFFTypeSet - try { - logger.info(`Saving static fulfillment ids in /${constants.ON_SEARCH}`) - - onSearchCatalog['bpp/providers'].forEach((provider: any) => { - const onSearchFFIds = new Set() - const bppFF = provider.fulfillments - const len = bppFF.length - - let i = 0 - while (i < len) { - onSearchFFTypeSet.add(bppFF[i].type) - onSearchFFIds.add(bppFF[i].id) - i++ + setValue(`${ApiSequence.ON_SEARCH}_context`, context) + setValue(`${ApiSequence.ON_SEARCH}_message`, message) + const providerOffers: any[] = message?.catalog['bpp/providers']?.flatMap((provider: any) => provider?.offers || []) + if (providerOffers && providerOffers.length > 0) { + setValue(`${ApiSequence.ON_SEARCH}_offers`, providerOffers) } - onSearchFFIdsArray.push(onSearchFFIds) - }) - - setValue('onSearchFFIdsArray', onSearchFFIdsArray) - } catch (error: any) { - logger.info(`Error while saving static fulfillment ids in /${constants.ON_SEARCH}, ${error.stack}`) - } - // Storing items of bpp/providers in itemsArray and itemIdList - try { - logger.info(`Storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - const items = provider.items - itemsArray.push(items) - items.forEach((item: any) => { - itemIdList.push(item.id) - }) - }) - setValue('ItemList', itemIdList) - setValue('onSearchItems', itemsArray) - } catch (error: any) { - logger.error( - `Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`, - ) - } + validateBapUri(context.bap_uri, context.bap_id, businessErrors) + validateBppUri(context.bpp_uri, context.bpp_id, businessErrors) + if (context.transaction_id == context.message_id) { + businessErrors['on_search_full_catalog_refresh'] = + `Context transaction_id (${context.transaction_id}) and message_id (${context.message_id}) can't be the same.` + } + if (!stateless) { + try { + logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(getValue(`${ApiSequence.SEARCH}_msgId`), context.message_id)) { + businessErrors[`${ApiSequence.ON_SEARCH}_msgId`] = + `Message Ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + } + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.ON_SEARCH}, ${error.stack}`) + } - // fulfillment type checking - try { - if(flow===FLOW.FLOW004){ - logger.info(`fulfillment type in bpp/providers in fulfillmentArray for /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - - provider.fulfillments.forEach((item: any) => { - if(item.type!== "Buyer-Delivery"){ - errorObj['missingFulfillmentType'] = `Fulfillment Type should be "Buyer-Delivery" for flow004 ` + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + businessErrors[`Domain[${data.context.action}]`] = `Domain should be same in each action` } - - + } - }) - }) - } - } catch (error: any) { - logger.error( - `Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`, - ) - } + const checkBap = checkBppIdOrBapId(context.bap_id) + const checkBpp = checkBppIdOrBapId(context.bpp_id) - //Checking item availability timings + if (checkBap) Object.assign(businessErrors, { bap_id: 'context/bap_id should not be a url' }) + if (checkBpp) Object.assign(businessErrors, { bpp_id: 'context/bpp_id should not be a url' }) - try { - if (flow === FLOW.FLOW001) { - logger.info(`Checking item availability timings in tags in items in bpp/providers for /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - const items = provider.items - items.forEach((it: any) => { - const timingTags = it.tags.find((item: any) => item.code === 'timing') - if (timingTags) { - errorObj['missingAvailabilityTimings'] = `Item availability timings should be present in items tags with code as 'timing' /${constants.ON_SEARCH}` + try { + logger.info(`Checking for context in /${constants.ON_SEARCH}`) + const contextRes: any = checkContext(context, constants.ON_SEARCH) + if (!contextRes?.valid) { + Object.assign(businessErrors, contextRes.ERRORS) } - }) - })} - } catch (error: any) { - logger.error(`Error while Checking item availability timings for /${constants.ON_SEARCH}, ${error.stack}`) - } - // Checking for mandatory Items in provider IDs - try { - const domain = context.domain.split(':')[1] - logger.info(`Checking for item tags in bpp/providers[0].items.tags in ${domain}`) - const isoDurationRegex = /^P(?=\d|T\d)(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$/ - for (let i in onSearchCatalog['bpp/providers']) { - const items = onSearchCatalog['bpp/providers'][i].items - items.forEach((item: any) => { - const replacementTerms = item.replacement_terms - - if (replacementTerms !== undefined) { - if (!Array.isArray(replacementTerms) || replacementTerms.length === 0) { - errorObj['on_search_full_catalog_refresh'] = - `replacement_terms must be a non-empty array if present for item ID '${item.id}'` - return - } + } catch (error: any) { + logger.error(`Error while checking for context in /${constants.ON_SEARCH}, ${error.stack}`) + } - for (const term of replacementTerms) { - if (!term.hasOwnProperty('replace_within')) { - errorObj['on_search_full_catalog_refresh'] = - `Missing 'replace_within' in replacement_terms for item ID '${item.id}'` - } + setValue(`${ApiSequence.ON_SEARCH}`, data) + const searchContext: any = stateless ? null : getValue(`${ApiSequence.SEARCH}_context`) - const duration = term.replace_within?.duration + try { + logger.info(`Storing BAP_ID and BPP_ID in /${constants.ON_SEARCH}`) + setValue('bapId', context.bap_id) + setValue('bppId', context.bpp_id) + } catch (error: any) { + logger.error(`!!Error while storing BAP and BPP Ids in /${constants.ON_SEARCH}, ${error.stack}`) + } - if (!duration || !isoDurationRegex.test(duration)) { - errorObj['on_search_full_catalog_refresh'] = - `Invalid or missing ISO 8601 duration in replacement_terms for item ID '${item.id}'. Found: '${duration}'` - } - } + try { + logger.info(`Comparing transaction Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(searchContext.transaction_id, context.transaction_id)) { + businessErrors.transaction_id = `Transaction Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` } - }) - let errors: any - switch (domain) { - case DOMAIN.RET10: - errors = checkMandatoryTags(i, items, errorObj, groceryJSON, 'Grocery') - break - case DOMAIN.RET12: - errors = checkMandatoryTags(i, items, errorObj, fashion, 'Fashion') - break - case DOMAIN.RET13: - errors = checkMandatoryTags(i, items, errorObj, BPCJSON, 'BPC') - break - case DOMAIN.RET14: - errors = checkMandatoryTags(i, items, errorObj, electronicsData, 'Electronics') - break - case DOMAIN.RET15: - errors = checkMandatoryTags(i, items, errorObj, applianceData, 'Appliances') - break - case DOMAIN.RET16: - errors = checkMandatoryTags(i, items, errorObj, homeJSON, 'Home & Kitchen') - break - case DOMAIN.RET18: - errors = checkMandatoryTags(i, items, errorObj, healthJSON, 'Health & Wellness') - break - case DOMAIN.AGR10: - errors = checkMandatoryTags(i, items, errorObj, agriJSON, 'Agriculture') - break - case DOMAIN.RET1A: - errors = checkMandatoryTags(i, items, errorObj, ret1aJSON, 'Automobile') - break + } catch (error: any) { + logger.info( + `Error while comparing transaction ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + ) } - Object.assign(errorObj, errors) - } - } catch (error: any) { - logger.error(`!!Errors while checking for items in bpp/providers/items, ${error.stack}`) - } - // Comparing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp - try { - logger.info(`Comparing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp`) - const timestamp = context.timestamp - for (let i in onSearchCatalog['bpp/providers']) { - const items = onSearchCatalog['bpp/providers'][i].items - items.forEach((item: any, index: number) => { - const itemTimeStamp = item.time.timestamp - const op = areTimestampsLessThanOrEqualTo(itemTimeStamp, timestamp) - if (!op) { - const key = `bpp/providers[${i}]/items/time/timestamp[${index}]` - errorObj[key] = `Timestamp for item[${index}] can't be greater than context.timestamp` - logger.error(`Timestamp for item[${index}] can't be greater than context.timestamp`) - } - }) - } - } catch (error: any) { - logger.error( - `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, - ) - } + // removed timestamp difference check + // try { + // logger.info(`Comparing timestamp of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + // const tmpstmp = searchContext?.timestamp + // if (_.gte(tmpstmp, context.timestamp)) { + // errorObj.tmpstmp = `Timestamp for /${constants.SEARCH} api cannot be greater than or equal to /${constants.ON_SEARCH} api` + // } else { + // const timeDiff = timeDifference(context.timestamp, tmpstmp) + // logger.info(timeDiff) + // if (timeDiff > 5000) { + // errorObj.tmpstmp = `context/timestamp difference between /${constants.ON_SEARCH} and /${constants.SEARCH} should be less than 5 sec` + // } + // } + // } catch (error: any) { + // logger.info( + // `Error while comparing timestamp for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + // ) + // } - // Checking for duplicate providerID in bpp/providers - try { - for (let i in onSearchCatalog['bpp/providers']) { - logger.info(`Validating uniqueness for provider id in bpp/providers[${i}]...`) - const prvdr = onSearchCatalog['bpp/providers'][i] - if (prvdrsId.has(prvdr.id)) { - const key = `prvdr${i}id` - errorObj[key] = `duplicate provider id: ${prvdr.id} in bpp/providers` - } else { - prvdrsId.add(prvdr.id) + try { + logger.info(`Comparing Message Ids of /${constants.SEARCH} and /${constants.ON_SEARCH}`) + if (!_.isEqual(searchContext.message_id, context.message_id)) { + errorObj.message_id = `Message Id for /${constants.SEARCH} and /${constants.ON_SEARCH} api should be same` + } + } catch (error: any) { + logger.info( + `Error while comparing message ids for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + ) } - } - setValue(`${ApiSequence.ON_SEARCH}prvdrsId`, prvdrsId) - } catch (error: any) { - logger.error(`!!Errors while checking provider id in bpp/providers, ${error.stack}`) - } - // Checking for long_desc and short_desc in bpp/providers/items/descriptor/ - try { - logger.info(`Checking for long_desc and short_desc in bpp/providers/items/descriptor/`) - for (let i in onSearchCatalog['bpp/providers']) { - const items = onSearchCatalog['bpp/providers'][i].items - items.forEach((item: any, index: number) => { - if (!item.descriptor.short_desc || !item.descriptor.long_desc) { - logger.error( - `short_desc and long_desc should not be provided as empty string "" in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor`, - ) - const key = `bpp/providers[${i}]/items[${index}]/descriptor` - errorObj[key] = - `short_desc and long_desc should not be provided as empty string "" in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor` - } - }) - } - } catch (error: any) { - logger.error( - `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, - ) - } - // Checking for code in bpp/providers/items/descriptor/ - try { - logger.info(`Checking for code in bpp/providers/items/descriptor/`) - for (let i in onSearchCatalog['bpp/providers']) { - const items = onSearchCatalog['bpp/providers'][i].items - items.forEach((item: any, index: number) => { - // Skip descriptor code validation for customization items - const isCustomization = isCustomizationItem(item) - - if (!isCustomization && !item.descriptor.code) { - logger.error(`code should be provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor`) - const key = `bpp/providers[${i}]/items[${index}]/descriptor` - errorObj[key] = `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor` - } else if (!isCustomization) { - const itemCodeArr = item.descriptor.code.split(':') - const itemDescType = itemCodeArr[0] - const itemDescCode = itemCodeArr[1] - const domain = getValue('domain') - const subdomain = domain?.substring(3) - if (domain != 'AGR10' && domain != 'RET1A') { - switch (subdomain) { - case '10': - case '13': - case '16': - case '18': - if (itemDescType != '1') { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should have 1:EAN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` - } else { - const regex = /^\d{8}$|^\d{13}$/ - if (!regex.test(itemDescCode)) { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code(${itemDescCode}) should be number and with either length 8 or 13` - } - } - break - case '12': - if (itemDescType == '4') { - const regex = /^\d{4}$|^\d{6}$|^\d{8}$|^\d{10}$/ - if (!regex.test(itemDescCode)) { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code should be number and have a length 4, 6, 8 or 10.` - } - } else { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should have 4:HSN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` - } - break - case '14': - case '15': - if (itemDescType == '3') { - const regex = /^\d{8}$|^\d{12}$|^\d{13}$|^\d{14}$/ - if (!regex.test(itemDescCode)) { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code should be number and have a length 8, 12, 13 or 14}.` - } - } else { - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should have 3:GTIN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` - } - break - default: - const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` - errorObj[key] = - `code should have a valid value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` - break - } - } - } - }) - } - } catch (error: any) { - logger.error( - `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/descriptor/code, ${error.stack}`, - ) - } + try { + const providers = data.message.catalog['bpp/providers'] + providers.forEach((provider: any) => { + const items = provider.items.forEach((item: any) => { + console.log('item inside items', item?.['@ondc/org/available_on_cod']) + }) - // Adding parent_item_id in a set - try { - logger.info(`Adding parent_item_id in a set on /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - provider.items.forEach((item: any) => { - if (!parentItemIdSet.has(item.parent_item_id)) { - parentItemIdSet.add(item.parent_item_id) - } - }) - }) + console.log('items>>', items) + console.log('provider items', provider.items) + const address = provider.locations[0].address - setValue('parentItemIdSet', parentItemIdSet) - } catch (error: any) { - logger.error(`Error while adding parent_item_id in a set on /${constants.ON_SEARCH}, ${error.stack}`) - } + if (address) { + const area_code = Number.parseInt(address.area_code) + const std = context.city.split(':')[1] - // Checking image array for bpp/providers/[]/categories/[]/descriptor/images[] - try { - logger.info(`Checking image array for bpp/provider/categories/descriptor/images[]`) - for (let i in onSearchCatalog['bpp/providers']) { - const categories = onSearchCatalog['bpp/providers'][i].categories - if (categories) { - categories.forEach((item: any, index: number) => { - if (item.descriptor.images && item.descriptor.images.length < 1) { - const key = `bpp/providers[${i}]/categories[${index}]/descriptor` - errorObj[key] = `Images should not be provided as empty array for categories[${index}]/descriptor` - logger.error(`Images should not be provided as empty array for categories[${index}]/descriptor`) + logger.info(`Comparing area_code and STD Code for /${constants.ON_SEARCH}`) + const areaWithSTD = compareSTDwithArea(area_code, std) + if (!areaWithSTD) { + logger.error(`STD code does not match with correct area_code in /${constants.ON_SEARCH}`) + errorObj.invldAreaCode = `STD code does not match with correct area_code in /${constants.ON_SEARCH}` + } } }) + } catch (error: any) { + logger.error( + `Error while matching area_code and std code for /${constants.SEARCH} and /${constants.ON_SEARCH} api, ${error.stack}`, + ) } - } - } catch (error: any) { - logger.error( - `!!Errors while checking image array for bpp/providers/[]/categories/[]/descriptor/images[], ${error.stack}`, - ) - } - - try { - logger.info(`Checking for np_type in bpp/descriptor`) - const descriptor = onSearchCatalog['bpp/descriptor'] - descriptor?.tags.map((tag: { code: any; list: any[] }) => { - if (tag.code === 'bpp_terms') { - const npType = tag.list.find((item) => item.code === 'np_type') - if (!npType) { - errorObj['bpp/descriptor'] = `Missing np_type in bpp/descriptor` - setValue(`${ApiSequence.ON_SEARCH}np_type`, '') - } else { - setValue(`${ApiSequence.ON_SEARCH}np_type`, npType.value) - } - const accept_bap_terms = tag.list.find((item) => item.code === 'accept_bap_terms') - if (accept_bap_terms) { - errorObj['bpp/descriptor/accept_bap_terms'] = - `accept_bap_terms is not required in bpp/descriptor/tags for now ` - } - // const collect_payment = tag.list.find((item) => item.code === 'collect_payment') - // if (collect_payment) { - // errorObj['bpp/descriptor/collect_payment'] = `collect_payment is not required in bpp/descriptor/tags for now ` - // } - } - if (flow === FLOW.FLOW007 || flow === FLOW.FLOW0099 || flow === OFFERSFLOW.FLOW0098) { - collect_payment_tags = tag.list.find((item) => item.code === 'collect_payment') - if (!collect_payment_tags) { - errorObj['bpp/descriptor/tags/collect_payment'] = - `collect_payment is required in bpp/descriptor/tags for on_search catalogue for flow: ${flow} ` - } - if (!['Y', 'N'].includes(collect_payment_tags.value)) { - errorObj['bpp/descriptor/tags/collect_payment'] = `value must be "Y" or "N" for flow: ${flow}` - } - setValue(collect_payment_tags.value, 'collect_payment') - } - }) - } catch (error: any) { - logger.error(`Error while checking np_type in bpp/descriptor for /${constants.ON_SEARCH}, ${error.stack}`) - } - //Validating Offers - try { - logger.info(`Checking offers under bpp/providers`) + const onSearchCatalog: any = message.catalog + const onSearchFFIdsArray: any = [] + const prvdrsId = new Set() + const prvdrLocId = new Set() + const itemsId = new Set() + const parentItemIdSet = new Set() + const onSearchFFTypeSet = new Set() + const itemsArray: any = [] + let itemIdList: any = [] + setValue('tmpstmp', context.timestamp) + + // Storing static fulfillment ids in onSearchFFIdsArray, OnSearchFFTypeSet + try { + logger.info(`Saving static fulfillment ids in /${constants.ON_SEARCH}`) + + onSearchCatalog['bpp/providers'].forEach((provider: any) => { + const onSearchFFIds = new Set() + const bppFF = provider.fulfillments + const len = bppFF.length + + let i = 0 + while (i < len) { + onSearchFFTypeSet.add(bppFF[i].type) + onSearchFFIds.add(bppFF[i].id) + i++ + } + onSearchFFIdsArray.push(onSearchFFIds) + }) - // Iterate through bpp/providers - for (let i in onSearchCatalog['bpp/providers']) { + setValue('onSearchFFIdsArray', onSearchFFIdsArray) + } catch (error: any) { + logger.info(`Error while saving static fulfillment ids in /${constants.ON_SEARCH}, ${error.stack}`) + } - const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? [] - if (!offers || !Array.isArray(offers)) { - const key = `bpp/providers[${i}]/offers` - errorObj[key] = `Offers must be an array in bpp/providers[${i}]` - continue + // Storing items of bpp/providers in itemsArray and itemIdList + try { + logger.info(`Storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + const items = provider.items + itemsArray.push(items) + items.forEach((item: any) => { + itemIdList.push(item.id) + }) + }) + setValue('ItemList', itemIdList) + setValue('onSearchItems', itemsArray) + } catch (error: any) { + logger.error( + `Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`, + ) } - if (offers.length > 0) { - if (!offersApplicableDomains.includes(context.domain)) { - const key = 'unsupportedDomain' - errorObj[key] = `Offers validation is only supported for domains: ${offersApplicableDomains.join(', ')}. Found: ${context.domain}` - return - } - offers.forEach((offer: any, offerIndex: number) => { - // Validate mandatory fields - if (!offer.id) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/id` - errorObj[key] = `Offer ID is mandatory for offers[${offerIndex}]` - logger.error(`Offer ID is mandatory for offers[${offerIndex}]`) - } - if (!offer.descriptor || !offer.descriptor.code) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/descriptor` - errorObj[key] = `Descriptor with code is mandatory for offers[${offerIndex}]` - logger.error(`Descriptor with code is mandatory for offers[${offerIndex}]`) - } + // fulfillment type checking + try { + if (flow === FLOW.FLOW004) { + logger.info(`fulfillment type in bpp/providers in fulfillmentArray for /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + + provider.fulfillments.forEach((item: any) => { + if (item.type !== "Buyer-Delivery") { + businessErrors['missingFulfillmentType'] = `Fulfillment Type should be "Buyer-Delivery" for flow004 ` + } - if (!offer.location_ids || !Array.isArray(offer.location_ids) || offer.location_ids.length === 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/location_ids` - errorObj[key] = `Location IDs array is mandatory for offers[${offerIndex}]` - logger.error(`Location IDs array is mandatory for offers[${offerIndex}]`) - } - if (!offer.item_ids || !Array.isArray(offer.item_ids) || offer.item_ids.length === 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/item_ids` - errorObj[key] = `Item IDs array is mandatory for offers[${offerIndex}]` - logger.error(`Item IDs array is mandatory for offers[${offerIndex}]`) - } - if (!offer.time || !offer.time.label || !offer.time.range || !offer.time.range.start || !offer.time.range.end) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/time` - errorObj[key] = `Time object with label and range (start/end) is mandatory for offers[${offerIndex}]` - logger.error(`Time object with label and range (start/end) is mandatory for offers[${offerIndex}]`) - } + }) + }) + } + } catch (error: any) { + logger.error( + `Error while storing items of bpp/providers in itemsArray for /${constants.ON_SEARCH}, ${error.stack}`, + ) + } - const tags = offer.tags - if (!tags || !Array.isArray(tags)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` - errorObj[key] = - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` - logger.error( - `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, - ) - return - } - const metaTagsError = validateMetaTags(tags) - if (metaTagsError) { - let i = 0 - const len = metaTagsError.length - while (i < len) { - const key = `metaTagsError${i}` - errorObj[key] = `${metaTagsError[i]}` - i++ - } - } + //Checking item availability timings - // Validate based on offer type - switch (offer.descriptor?.code) { - case 'discount': - // Validate 'qualifier' - const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + try { + if (flow === FLOW.FLOW001) { + logger.info(`Checking item availability timings in tags in items in bpp/providers for /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + const items = provider.items + items.forEach((it: any) => { + const timingTags = it.tags.find((item: any) => item.code === 'timing') + if (timingTags) { + businessErrors['missingAvailabilityTimings'] = `Item availability timings should be present in items tags with code as 'timing' /${constants.ON_SEARCH}` + } + }) + }) + } + } catch (error: any) { + logger.error(`Error while Checking item availability timings for /${constants.ON_SEARCH}, ${error.stack}`) + } + // Checking for mandatory Items in provider IDs + try { + const domain = context.domain.split(':')[1] + logger.info(`Checking for item tags in bpp/providers[0].items.tags in ${domain}`) + const isoDurationRegex = /^P(?=\d|T\d)(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$/ + for (let i in onSearchCatalog['bpp/providers']) { + const items = onSearchCatalog['bpp/providers'][i].items + items.forEach((item: any) => { + const replacementTerms = item.replacement_terms + + if (replacementTerms !== undefined) { + if (!Array.isArray(replacementTerms) || replacementTerms.length === 0) { + businessErrors['on_search_full_catalog_refresh'] = + `replacement_terms must be a non-empty array if present for item ID '${item.id}'` + return } - // Validate 'benefit' - const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') - if (!benefitDiscount) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag is required for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag is required for offers[${offerIndex}]`) - } else { - const valueTypeItem = benefitDiscount.list.find((item: any) => item.code === 'value_type') - const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value') - const valueCapItem = benefitDiscount.list.find((item: any) => item.code === 'value_cap') - - if (!valueTypeItem) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_type` - errorObj[key] = `'value_type' is required in benefit tag for offers[${offerIndex}]` - logger.error(`'value_type' is required in benefit tag for offers[${offerIndex}]`) + for (const term of replacementTerms) { + if (!term.hasOwnProperty('replace_within')) { + businessErrors['on_search_full_catalog_refresh'] = + `Missing 'replace_within' in replacement_terms for item ID '${item.id}'` } - if (!valueItem) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' is required in benefit tag for offers[${offerIndex}]` - logger.error(`'value' is required in benefit tag for offers[${offerIndex}]`) - } else { - // Validate value is a proper number - const value = valueItem.value - if (isNaN(parseFloat(value)) || !/^-?\d+(\.\d+)?$/.test(value)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' in benefit tag must be a valid number for offers[${offerIndex}]` - logger.error(`'value' in benefit tag must be a valid number for offers[${offerIndex}]`) - } else if (parseFloat(value) >= 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` - errorObj[key] = `'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]` - logger.error(`'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]`) - } - } + const duration = term.replace_within?.duration - // Validate value_cap if present - if (valueCapItem) { - const valueCap = valueCapItem.value - if (isNaN(parseFloat(valueCap)) || !/^-?\d+(\.\d+)?$/.test(valueCap)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_cap` - errorObj[key] = `'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]` - logger.error(`'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]`) - } + if (!duration || !isoDurationRegex.test(duration)) { + businessErrors['on_search_full_catalog_refresh'] = + `Invalid or missing ISO 8601 duration in replacement_terms for item ID '${item.id}'. Found: '${duration}'` } } + } + }) + let errors: any + switch (domain) { + case DOMAIN.RET10: + errors = checkMandatoryTags(i, items, errorObj, groceryJSON, 'Grocery') break - - case 'buyXgetY': - // Validate 'qualifier' - const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierBuyXgetY || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`) - } - - // Validate 'benefit' - const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') - if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) - } + case DOMAIN.RET12: + errors = checkMandatoryTags(i, items, errorObj, fashion, 'Fashion') break - - case 'freebie': - // Validate 'qualifier' - const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } - - // Validate 'benefit' - const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitFreebie || - !benefitFreebie.list.some((item: any) => item.code === 'item_count') || - !benefitFreebie.list.some((item: any) => item.code === 'item_id') || - !benefitFreebie.list.some((item: any) => item.code === 'item_value') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, - ) - } + case DOMAIN.RET13: + errors = checkMandatoryTags(i, items, errorObj, BPCJSON, 'BPC') break - - case 'slab': - // Validate 'qualifier' - const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } - - // Validate 'benefit' - const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitSlab || - !benefitSlab.list.some((item: any) => item.code === 'value') || - !benefitSlab.list.some((item: any) => item.code === 'value_type') || - !benefitSlab.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) - } + case DOMAIN.RET14: + errors = checkMandatoryTags(i, items, errorObj, electronicsData, 'Electronics') break - - case 'combo': - // Validate 'qualifier' - const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') - if ( - !qualifierCombo || - !qualifierCombo.list.some((item: any) => item.code === 'min_value') || - !qualifierCombo.list.some((item: any) => item.code === 'item_id') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) - } - - // Validate 'benefit' - const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitCombo || - !benefitCombo.list.some((item: any) => item.code === 'value') || - !benefitCombo.list.some((item: any) => item.code === 'value_type') || - !benefitCombo.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error( - `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, - ) - } + case DOMAIN.RET15: + errors = checkMandatoryTags(i, items, errorObj, applianceData, 'Appliances') break - - case 'delivery': - // Validate 'qualifier' - const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') - if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - errorObj[key] = - `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - } - - // Validate 'benefit' - const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') - if ( - !benefitDelivery || - !benefitDelivery.list.some((item: any) => item.code === 'value') || - !benefitDelivery.list.some((item: any) => item.code === 'value_type') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - errorObj[key] = - `'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - logger.error(`'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}]`) - } + case DOMAIN.RET16: + errors = checkMandatoryTags(i, items, errorObj, homeJSON, 'Home & Kitchen') break - - case 'exchange': - // Validate 'qualifier' - // const qualifierExchange = tags.find((tag: any) => tag.code === 'qualifier') - // if (!qualifierExchange || !qualifierExchange.list.some((item: any) => item.code === 'min_value')) { - // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` - // errorObj[key] = - // `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) - // } - - // // Validate that benefits should not exist or should be empty - // const benefitExchange = tags.find((tag: any) => tag.code === 'benefit') - // if (benefitExchange && benefitExchange.list.length > 0) { - // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` - // errorObj[key] = - // `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` - // logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`) - // } - if (context.domain !== 'ONDC:RET14' || context.domain !== 'ONDC:RET15') { - errorObj['unsupportedDomain'] = - `exchange is not possible for ${context.domain} as supported domains are 'ONDC:RET14','ONDC:RET15' is required for flow: ${flow}` - } + case DOMAIN.RET18: + errors = checkMandatoryTags(i, items, errorObj, healthJSON, 'Health & Wellness') break - case 'financing': - let collect_payment_value = collect_payment_tags?.value - if (flow === FLOW.FLOW0099 || collect_payment_value === 'no') { - const financeTagsError = validateFinanceTags(tags) - if (financeTagsError) { - let i = 0 - const len = financeTagsError.length - while (i < len) { - const key = `financeTagsError${i}` - errorObj[key] = `${financeTagsError[i]}` - i++ - } - } - } + case DOMAIN.AGR10: + errors = checkMandatoryTags(i, items, errorObj, agriJSON, 'Agriculture') + break + case DOMAIN.RET1A: + errors = checkMandatoryTags(i, items, errorObj, ret1aJSON, 'Automobile') break - default: - logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) } - }) + Object.assign(businessErrors, errors) + } + } catch (error: any) { + logger.error(`!!Errors while checking for items in bpp/providers/items, ${error.stack}`) } - } - } catch (error: any) { - logger.error(`Error while checking offers under bpp/providers: ${error.stack}`) - } - // Checking price of items in bpp/providers - try { - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any, i: number) => { - const items = provider.items - items.forEach((item: any, j: number) => { - // Skip price validation for customization items - const isCustomization = isCustomizationItem(item) - - if (!isCustomization && item.price && item.price.value) { - const priceValue = parseFloat(item.price.value) - if (priceValue < 1) { - const key = `prvdr${i}item${j}price` - errorObj[key] = `item.price.value should be greater than 0` - } + // Comparing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp + try { + logger.info(`Comparing valid timestamp in context.timestamp and bpp/providers/items/time/timestamp`) + const timestamp = context.timestamp + for (let i in onSearchCatalog['bpp/providers']) { + const items = onSearchCatalog['bpp/providers'][i].items + items.forEach((item: any, index: number) => { + const itemTimeStamp = item.time.timestamp + const op = areTimestampsLessThanOrEqualTo(itemTimeStamp, timestamp) + if (!op) { + const key = `bpp/providers[${i}]/items/time/timestamp[${index}]` + businessErrors[key] = `Timestamp for item[${index}] can't be greater than context.timestamp` + logger.error(`Timestamp for item[${index}] can't be greater than context.timestamp`) + } + }) } - }) - }) - } catch (error: any) { - logger.error(`Error while checking price of items in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`) - } - - // Mapping items with thier respective providers - try { - const itemProviderMap: any = {} - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any) => { - const items = provider.items - const itemArray: any = [] - items.forEach((item: any) => { - itemArray.push(item.id) - }) - itemProviderMap[provider.id] = itemArray - }) - - setValue('itemProviderMap', itemProviderMap) - } catch (e: any) { - logger.error(`Error while mapping items with thier respective providers ${e.stack}`) - } - - // Checking for quantity of items in bpp/providers - try { - logger.info(`Checking for quantity of items in bpp/providers for /${constants.ON_SEARCH}`) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any, i: number) => { - const items = provider.items - function getProviderCreds() { - return providers.map((provider: any) => { - const creds = provider?.creds - if ((flow === FLOW.FLOW017 && creds === undefined) || creds.length === 0) { - errorObj['MissingCreds'] = `creds must be present in order in /${constants.ON_SEARCH}` - } + } catch (error: any) { + logger.error( + `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, + ) + } - return { - providerId: provider.id, - creds, + // Checking for duplicate providerID in bpp/providers + try { + for (let i in onSearchCatalog['bpp/providers']) { + logger.info(`Validating uniqueness for provider id in bpp/providers[${i}]...`) + const prvdr = onSearchCatalog['bpp/providers'][i] + if (prvdrsId.has(prvdr.id)) { + const key = `prvdr${i}id` + businessErrors[key] = `duplicate provider id: ${prvdr.id} in bpp/providers` + } else { + prvdrsId.add(prvdr.id) } - }) + } + setValue(`${ApiSequence.ON_SEARCH}prvdrsId`, prvdrsId) + } catch (error: any) { + logger.error(`!!Errors while checking provider id in bpp/providers, ${error.stack}`) } - const result = getProviderCreds() - setValue('credsWithProviderId', result) - - items.forEach((item: any, j: number) => { - if (item.quantity && item.quantity.available && typeof item.quantity.available.count === 'string') { - const availCount = parseInt(item.quantity.available.count, 10) - const maxCount = parseInt(item.quantity.maximum.count, 10) - const minCount = parseInt(item.quantity.minimum.count, 10) - if (!minCount) { - const key = `prvdr${i}item${j}minimum.count` - errorObj[key] = `item.quantity.minimum.count must be added , if not set default as 99 ` - } - if (item.quantity.unitized.measure.value < 1) { - const key = `prvdr${i}item${j}unitized` - errorObj[key] = `item.quantity.unitized.measure.value should be greater than 0` - } - if (availCount < 0 || maxCount < 0) { - const key = `prvdr${i}item${j}invldCount` - errorObj[key] = - `item.quantity.available.count and item.quantity.maximum.count should be greater than or equal to 0` - } + // Checking for long_desc and short_desc in bpp/providers/items/descriptor/ + try { + logger.info(`Checking for long_desc and short_desc in bpp/providers/items/descriptor/`) + for (let i in onSearchCatalog['bpp/providers']) { + const items = onSearchCatalog['bpp/providers'][i].items + items.forEach((item: any, index: number) => { + if (!item.descriptor.short_desc || !item.descriptor.long_desc) { + logger.error( + `short_desc and long_desc should not be provided as empty string "" in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor`, + ) + const key = `bpp/providers[${i}]/items[${index}]/descriptor` + businessErrors[key] = + `short_desc and long_desc should not be provided as empty string "" in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor` + } + }) + } + } catch (error: any) { + logger.error( + `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/time/timestamp, ${error.stack}`, + ) + } + // Checking for code in bpp/providers/items/descriptor/ + try { + logger.info(`Checking for code in bpp/providers/items/descriptor/`) + for (let i in onSearchCatalog['bpp/providers']) { + const items = onSearchCatalog['bpp/providers'][i].items + items.forEach((item: any, index: number) => { + // Skip descriptor code validation for customization items + const isCustomization = isCustomizationItem(item) + + if (!isCustomization && !item.descriptor.code) { + logger.error(`code should be provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor`) + const key = `bpp/providers[${i}]/items[${index}]/descriptor` + businessErrors[key] = `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor` + } else if (!isCustomization) { + const itemCodeArr = item.descriptor.code.split(':') + const itemDescType = itemCodeArr[0] + const itemDescCode = itemCodeArr[1] + const domain = getValue('domain') + const subdomain = domain?.substring(3) + if (domain != 'AGR10' && domain != 'RET1A') { + switch (subdomain) { + case '10': + case '13': + case '16': + case '18': + if (itemDescType != '1') { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should have 1:EAN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` + } else { + const regex = /^\d{8}$|^\d{13}$/ + if (!regex.test(itemDescCode)) { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code(${itemDescCode}) should be number and with either length 8 or 13` + } + } + break + case '12': + if (itemDescType == '4') { + const regex = /^\d{4}$|^\d{6}$|^\d{8}$|^\d{10}$/ + if (!regex.test(itemDescCode)) { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code should be number and have a length 4, 6, 8 or 10.` + } + } else { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should have 4:HSN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` + } + break + case '14': + case '15': + if (itemDescType == '3') { + const regex = /^\d{8}$|^\d{12}$|^\d{13}$|^\d{14}$/ + if (!regex.test(itemDescCode)) { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should provided in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code should be number and have a length 8, 12, 13 or 14}.` + } + } else { + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should have 3:GTIN as a value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` + } + break + default: + const key = `bpp/providers[${i}]/items[${index}]/descriptor/code` + businessErrors[key] = + `code should have a valid value in /message/catalog/bpp/providers[${i}]/items[${index}]/descriptor/code` + break + } + } + } + }) } - }) - }) - } catch (error: any) { - logger.error(`Error while checking quantity of items in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`) - } + } catch (error: any) { + logger.error( + `!!Errors while checking timestamp in context.timestamp and bpp/providers/items/descriptor/code, ${error.stack}`, + ) + } - // Checking for items categoryId and it's mapping with statutory_reqs - if (getValue('domain') === 'RET10') { - try { - logger.info( - `Checking for items categoryId and it's mapping with statutory_reqs in bpp/providers for /${constants.ON_SEARCH}`, - ) - const providers = onSearchCatalog['bpp/providers'] - providers.forEach((provider: any, i: number) => { - const items = provider.items - items.forEach((item: any, j: number) => { - if (!_.isEmpty(item?.category_id)) { - const statutoryRequirement: any = getStatutoryRequirement(item.category_id) - let errors: any - switch (statutoryRequirement) { - case statutory_reqs.PrepackagedFood: - errors = checkForStatutory(item, i, j, errorObj, statutory_reqs.PrepackagedFood) - break - case statutory_reqs.PackagedCommodities: - errors = checkForStatutory(item, i, j, errorObj, statutory_reqs.PackagedCommodities) - break - case statutory_reqs.None: - break - default: - const key = `prvdr${i}item${j}statutoryReq` - errorObj[key] = - `The following item/category_id is not a valid one in bpp/providers for /${constants.ON_SEARCH}` - break + // Adding parent_item_id in a set + try { + logger.info(`Adding parent_item_id in a set on /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + provider.items.forEach((item: any) => { + if (!parentItemIdSet.has(item.parent_item_id)) { + parentItemIdSet.add(item.parent_item_id) } - Object.assign(errorObj, errors) - } + }) }) - }) - } catch (error: any) { - logger.error( - `Error while checking for items categoryId and it's mapping with statutory_reqs in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`, - ) - } - } - // Checking for duplicate varient in bpp/providers/items for on_search - // try { - // logger.info(`Checking for duplicate varient in bpp/providers/items for on_search`) - // for (let i in onSearchCatalog['bpp/providers']) { - // const varientPath: any = findVariantPath(onSearchCatalog['bpp/providers'][i].categories) - // const items = onSearchCatalog['bpp/providers'][i].items - // const map = checkDuplicateParentIdItems(items) - // for (let key in map) { - // if (map[key].length > 1) { - // const item = varientPath.find((item: any) => { - // return item.item_id === key - // }) - // const pathForVarient = item.paths - // let valueArray = [] - // if (pathForVarient.length) { - // for (let j = 0; j < map[key].length; j++) { - // let itemValues: any = {} - // for (let path of pathForVarient) { - // if (path === 'item.quantity.unitized.measure') { - // const unit = map[key][j].quantity.unitized.measure.unit - // const value = map[key][j].quantity.unitized.measure.value - // itemValues['unit'] = unit - // itemValues['value'] = value - // } else { - // const val = findValueAtPath(path, map[key][j]) - // itemValues[path.split('.').pop()] = val - // } - // } - // valueArray.push(itemValues) - // } - // checkForDuplicates(valueArray, errorObj) - // } - // } - // } - // } - // } catch (error: any) { - // logger.error(`!!Errors while checking parent_item_id in bpp/providers/[]/items/[]/parent_item_id/, ${error.stack}`) - // } + setValue('parentItemIdSet', parentItemIdSet) + } catch (error: any) { + logger.error(`Error while adding parent_item_id in a set on /${constants.ON_SEARCH}, ${error.stack}`) + } - try { - logger.info(`Checking Providers info (bpp/providers) in /${constants.ON_SEARCH}`) - let i = 0 - const bppPrvdrs = onSearchCatalog['bpp/providers'] - const len = bppPrvdrs.length - const tmpstmp = context.timestamp - - while (i < len) { - const categoriesId = new Set() - const customGrpId = new Set() - const seqSet = new Set() - const itemCategory_id = new Set() - const categoryRankSet = new Set() - const prvdrLocationIds = new Set() - const prvdr = bppPrvdrs[i] - - logger.info(`Checking store enable/disable timestamp in bpp/providers[${i}]`) + // Checking image array for bpp/providers/[]/categories/[]/descriptor/images[] try { - if (prvdr.time) { - const providerTime = new Date(prvdr.time.timestamp).getTime() - const contextTimestamp = new Date(tmpstmp).getTime() - if (providerTime > contextTimestamp) { - errorObj.StoreEnableDisable = `store enable/disable timestamp (/bpp/providers/time/timestamp) should be less then or equal to context.timestamp` + logger.info(`Checking image array for bpp/provider/categories/descriptor/images[]`) + for (let i in onSearchCatalog['bpp/providers']) { + const categories = onSearchCatalog['bpp/providers'][i].categories + if (categories) { + categories.forEach((item: any, index: number) => { + if (item.descriptor.images && item.descriptor.images.length < 1) { + const key = `bpp/providers[${i}]/categories[${index}]/descriptor` + businessErrors[key] = `Images should not be provided as empty array for categories[${index}]/descriptor` + logger.error(`Images should not be provided as empty array for categories[${index}]/descriptor`) + } + }) } } } catch (error: any) { - logger.error(`Error while checking store enable/disable timestamp in bpp/providers[${i}]`, error) + logger.error( + `!!Errors while checking image array for bpp/providers/[]/categories/[]/descriptor/images[], ${error.stack}`, + ) } - logger.info(`Checking store timings in bpp/providers[${i}]`) - - prvdr.locations.forEach((loc: any, iter: any) => { - try { - logger.info(`Checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`) - const has = Object.prototype.hasOwnProperty - if (has.call(loc, 'gps')) { - if (!checkGpsPrecision(loc.gps)) { - errorObj.gpsPrecision = `/bpp/providers[${i}]/locations[${iter}]/gps coordinates must be specified with at least 4 decimal places of precision.` + try { + logger.info(`Checking for np_type in bpp/descriptor`) + const descriptor = onSearchCatalog['bpp/descriptor'] + descriptor?.tags.map((tag: { code: any; list: any[] }) => { + if (tag.code === 'bpp_terms') { + const npType = tag.list.find((item) => item.code === 'np_type') + if (!npType) { + businessErrors['bpp/descriptor'] = `Missing np_type in bpp/descriptor` + setValue(`${ApiSequence.ON_SEARCH}np_type`, '') + } else { + setValue(`${ApiSequence.ON_SEARCH}np_type`, npType.value) + } + const accept_bap_terms = tag.list.find((item) => item.code === 'accept_bap_terms') + if (accept_bap_terms) { + businessErrors['bpp/descriptor/accept_bap_terms'] = + `accept_bap_terms is not required in bpp/descriptor/tags for now ` } + // const collect_payment = tag.list.find((item) => item.code === 'collect_payment') + // if (collect_payment) { + // businessErrors['bpp/descriptor/collect_payment'] = `collect_payment is not required in bpp/descriptor/tags for now ` + // } } - } catch (error) { - logger.error( - `!!Error while checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`, - error, - ) - } + if (flow === FLOW.FLOW007 || flow === FLOW.FLOW0099 || flow === OFFERSFLOW.FLOW0098) { + collect_payment_tags = tag.list.find((item) => item.code === 'collect_payment') + if (!collect_payment_tags) { + businessErrors['bpp/descriptor/tags/collect_payment'] = + `collect_payment is required in bpp/descriptor/tags for on_search catalogue for flow: ${flow} ` + } + if (!['Y', 'N'].includes(collect_payment_tags.value)) { + businessErrors['bpp/descriptor/tags/collect_payment'] = `value must be "Y" or "N" for flow: ${flow}` + } + setValue(collect_payment_tags.value, 'collect_payment') + } + }) + } catch (error: any) { + logger.error(`Error while checking np_type in bpp/descriptor for /${constants.ON_SEARCH}, ${error.stack}`) + } - try { - if (prvdrLocId.has(loc?.id)) { - const key = `prvdr${i}${loc.id}${iter}` - errorObj[key] = `duplicate location id: ${loc.id} in /bpp/providers[${i}]/locations[${iter}]` - } else { - prvdrLocId.add(loc.id) + //Validating Offers + try { + logger.info(`Checking offers under bpp/providers`) + + // Iterate through bpp/providers + for (let i in onSearchCatalog['bpp/providers']) { + + const offers = onSearchCatalog['bpp/providers'][i]?.offers ?? [] + if (!offers || !Array.isArray(offers)) { + const key = `bpp/providers[${i}]/offers` + businessErrors[key] = `Offers must be an array in bpp/providers[${i}]` + continue } - prvdrLocationIds.add(loc?.id) - logger.info('Checking store days...') - const days = loc?.time?.days?.split(',') - days.forEach((day: any) => { - day = parseInt(day) - if (isNaN(day) || day < 1 || day > 7) { - const key = `prvdr${i}locdays${iter}` - errorObj[key] = - `store days (bpp/providers[${i}]/locations[${iter}]/time/days) should be in the format ("1,2,3,4,5,6,7") where 1- Monday and 7- Sunday` + if (offers.length > 0) { + if (!offersApplicableDomains.includes(context.domain)) { + const key = 'unsupportedDomain' + businessErrors[key] = `Offers validation is only supported for domains: ${offersApplicableDomains.join(', ')}. Found: ${context.domain}` + return } - }) + offers.forEach((offer: any, offerIndex: number) => { + // Validate mandatory fields + if (!offer.id) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/id` + businessErrors[key] = `Offer ID is mandatory for offers[${offerIndex}]` + logger.error(`Offer ID is mandatory for offers[${offerIndex}]`) + } - logger.info('Checking fixed or split timings') + if (!offer.descriptor || !offer.descriptor.code) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/descriptor` + businessErrors[key] = `Descriptor with code is mandatory for offers[${offerIndex}]` + logger.error(`Descriptor with code is mandatory for offers[${offerIndex}]`) + } - //scenario 1: range =1 freq/times =1 - if (loc?.time?.range && (loc.time?.schedule?.frequency || loc.time?.schedule?.times)) { - const key = `prvdr${i}loctime${iter}` - errorObj[key] = - `Either one of fixed (range) or split (frequency and times) timings should be provided in /bpp/providers[${i}]/locations[${iter}]/time` - } + if (!offer.location_ids || !Array.isArray(offer.location_ids) || offer.location_ids.length === 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/location_ids` + businessErrors[key] = `Location IDs array is mandatory for offers[${offerIndex}]` + logger.error(`Location IDs array is mandatory for offers[${offerIndex}]`) + } - // scenario 2: range=0 freq || times =1 - if (!loc?.time?.range && (!loc?.time?.schedule?.frequency || !loc?.time?.schedule?.times)) { - const key = `prvdr${i}loctime${iter}` - errorObj[key] = - `Either one of fixed timings (range) or split timings (both frequency and times) should be provided in /bpp/providers[${i}]/locations[${iter}]/time` - } + if (!offer.item_ids || !Array.isArray(offer.item_ids) || offer.item_ids.length === 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/item_ids` + businessErrors[key] = `Item IDs array is mandatory for offers[${offerIndex}]` + logger.error(`Item IDs array is mandatory for offers[${offerIndex}]`) + } - //scenario 3: range=1 (start and end not compliant) frequency=0; - if ('range' in loc.time) { - logger.info('checking range (fixed timings) start and end') - const startTime: any = 'start' in loc?.time?.range ? parseInt(loc?.time?.range?.start) : '' - const endTime: any = 'end' in loc?.time?.range ? parseInt(loc?.time?.range?.end) : '' - if (isNaN(startTime) || isNaN(endTime) || startTime > endTime || endTime > 2359) { - errorObj.startEndTime = `end time must be greater than start time in fixed timings /locations/time/range (fixed store timings)` - } - } - } catch (error: any) { - logger.error(`Validation error for frequency: ${error.stack}`) - } - }) + if (!offer.time || !offer.time.label || !offer.time.range || !offer.time.range.start || !offer.time.range.end) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/time` + businessErrors[key] = `Time object with label and range (start/end) is mandatory for offers[${offerIndex}]` + logger.error(`Time object with label and range (start/end) is mandatory for offers[${offerIndex}]`) + } - try { - logger.info(`Checking for upcoming holidays`) - const location = onSearchCatalog['bpp/providers'][i]['locations'] - if (!location) { - logger.error('No location detected ') - } + const tags = offer.tags + if (!tags || !Array.isArray(tags)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags` + businessErrors[key] = + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'` + logger.error( + `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`, + ) + return + } + const metaTagsError = validateMetaTags(tags) + if (metaTagsError) { + let i = 0 + const len = metaTagsError.length + while (i < len) { + const key = `metaTagsError${i}` + businessErrors[key] = `${metaTagsError[i]}` + i++ + } + } - const scheduleObject = location[i].time.schedule.holidays - const timestamp = context.timestamp - const [currentDate] = timestamp.split('T') - - scheduleObject.map((date: string) => { - const dateObj = new Date(date) - const currentDateObj = new Date(currentDate) - if (dateObj.getTime() < currentDateObj.getTime()) { - const key = `/message/catalog/bpp/providers/loc${i}/time/schedule/holidays` - errorObj[key] = `Holidays cannot be past ${currentDate}` - } - }) - } catch (e) { - logger.error('No Holiday', e) - } + // Validate based on offer type + switch (offer.descriptor?.code) { + case 'discount': + // Validate 'qualifier' + const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + } - try { - logger.info(`Checking categories for provider (${prvdr.id}) in bpp/providers[${i}]`) - let j = 0 - const categories = onSearchCatalog['bpp/providers'][i]['categories'] - if (!categories || !categories.length) { - const key = `prvdr${i}categories` - errorObj[key] = `Support for variants is mandatory, categories must be present in bpp/providers[${i}]` - } - const iLen = categories?.length - while (j < iLen) { - logger.info(`Validating uniqueness for categories id in bpp/providers[${i}].items[${j}]...`) - const category = categories[j] - - const fulfillments = onSearchCatalog['bpp/providers'][i]['fulfillments'] - const phoneNumber = fulfillments[i].contact.phone - - if (!isValidPhoneNumber(phoneNumber)) { - const key = `bpp/providers${i}fulfillments${i}` - errorObj[key] = - `Please enter a valid phone number consisting of 10 or 11 digits without any spaces or special characters. ` - } + // Validate 'benefit' + const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit') + if (!benefitDiscount) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag is required for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag is required for offers[${offerIndex}]`) + } else { + const valueTypeItem = benefitDiscount.list.find((item: any) => item.code === 'value_type') + const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value') + const valueCapItem = benefitDiscount.list.find((item: any) => item.code === 'value_cap') + + if (!valueTypeItem) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_type` + businessErrors[key] = `'value_type' is required in benefit tag for offers[${offerIndex}]` + logger.error(`'value_type' is required in benefit tag for offers[${offerIndex}]`) + } - if (categoriesId.has(category.id)) { - const key = `prvdr${i}category${j}` - errorObj[key] = `duplicate category id: ${category.id} in bpp/providers[${i}]` - } else { - categoriesId.add(category.id) - } + if (!valueItem) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' is required in benefit tag for offers[${offerIndex}]` + logger.error(`'value' is required in benefit tag for offers[${offerIndex}]`) + } else { + // Validate value is a proper number + const value = valueItem.value + if (isNaN(parseFloat(value)) || !/^-?\d+(\.\d+)?$/.test(value)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' in benefit tag must be a valid number for offers[${offerIndex}]` + logger.error(`'value' in benefit tag must be a valid number for offers[${offerIndex}]`) + } else if (parseFloat(value) >= 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value` + businessErrors[key] = `'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]` + logger.error(`'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]`) + } + } - try { - category.tags.map((tag: { code: any; list: any[] }, index: number) => { - switch (tag.code) { - case 'attr': - const attrList = tag.list.find((item) => item.code === 'name') - if (attrList) { - const isValid = - attrList.value === 'item.quantity.unitized.measure' || - attrList.value.startsWith('item.tags.attribute') - - if (!isValid) { - const key = `prvdr${i}category${j}tags${index}` - errorObj[key] = - `list.code == attr then name should be 'item.quantity.unitized.measure' or 'item.tags.attribute.{object.keys}' in bpp/providers[${i}]` + // Validate value_cap if present + if (valueCapItem) { + const valueCap = valueCapItem.value + if (isNaN(parseFloat(valueCap)) || !/^-?\d+(\.\d+)?$/.test(valueCap)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value_cap` + businessErrors[key] = `'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]` + logger.error(`'value_cap' in benefit tag must be a valid number for offers[${offerIndex}]`) + } } } break - case 'type': - const codeList = tag.list.find((item) => item.code === 'type') + + case 'buyXgetY': + // Validate 'qualifier' + const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier') if ( - !( - codeList.value === 'custom_menu' || - codeList.value === 'custom_group' || - codeList.value === 'variant_group' - ) + !qualifierBuyXgetY || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') ) { - const key = `prvdr${i}category${j}tags${index}` - errorObj[key] = - `list.code == type then value should be one of 'custom_menu','custom_group' and 'variant_group' in bpp/providers[${i}]` + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`) } - if (codeList.value === 'custom_group') { - customGrpId.add(category.id) + // Validate 'benefit' + const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit') + if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`) } - break - case 'timing': - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj.custom_menu_timing_tag = `Invalid value for '${item.code}': ${item.value}` - } - - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - errorObj.time_to = `Invalid time format for '${item.code}': ${item.value}` - } - break - default: - errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` - } + case 'freebie': + // Validate 'qualifier' + const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) } - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') - - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) - - if (dayTo < dayFrom) { - errorObj.day_from = "'day_to' must be greater than or equal to 'day_from'" - } - - if (timeTo <= timeFrom) { - errorObj.time_from = "'time_to' must be greater than 'time_from'" - } + // Validate 'benefit' + const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitFreebie || + !benefitFreebie.list.some((item: any) => item.code === 'item_count') || + !benefitFreebie.list.some((item: any) => item.code === 'item_id') || + !benefitFreebie.list.some((item: any) => item.code === 'item_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`, + ) } - break - case 'display': - for (const item of tag.list) { - if (item.code !== 'rank' || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj.rank = `Invalid value for 'display': ${item.value}` - } else { - if (categoryRankSet.has(category.id)) { - const key = `prvdr${i}category${j}rank` - errorObj[key] = `duplicate rank in category id: ${category.id} in bpp/providers[${i}]` - } else { - categoryRankSet.add(category.id) - } - } + + case 'slab': + // Validate 'qualifier' + const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) } - break - case 'config': - const minItem: any = tag.list.find((item: { code: string }) => item.code === 'min') - const maxItem: any = tag.list.find((item: { code: string }) => item.code === 'max') - const inputItem: any = tag.list.find((item: { code: string }) => item.code === 'input') - const seqItem: any = tag.list.find((item: { code: string }) => item.code === 'seq') - - if (!minItem || !maxItem) { - errorObj[`customization_config_${j}`] = - `Both 'min' and 'max' values are required in 'config' at index: ${j}` + // Validate 'benefit' + const benefitSlab = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitSlab || + !benefitSlab.list.some((item: any) => item.code === 'value') || + !benefitSlab.list.some((item: any) => item.code === 'value_type') || + !benefitSlab.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) } + break - if (!/^-?\d+(\.\d+)?$/.test(minItem.value)) { - errorObj[`customization_config_min_${j}`] = - `Invalid value for ${minItem.code}: ${minItem.value} at index: ${j}` + case 'combo': + // Validate 'qualifier' + const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier') + if ( + !qualifierCombo || + !qualifierCombo.list.some((item: any) => item.code === 'min_value') || + !qualifierCombo.list.some((item: any) => item.code === 'item_id') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`) } - if (!/^-?\d+(\.\d+)?$/.test(maxItem.value)) { - errorObj[`customization_config_max_${j}`] = - `Invalid value for ${maxItem.code}: ${maxItem.value}at index: ${j}` + // Validate 'benefit' + const benefitCombo = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitCombo || + !benefitCombo.list.some((item: any) => item.code === 'value') || + !benefitCombo.list.some((item: any) => item.code === 'value_type') || + !benefitCombo.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error( + `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`, + ) } + break - if (!/^-?\d+(\.\d+)?$/.test(seqItem.value)) { - errorObj[`config_seq_${j}`] = `Invalid value for ${seqItem.code}: ${seqItem.value} at index: ${j}` + case 'delivery': + // Validate 'qualifier' + const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier') + if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + businessErrors[key] = + `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) } - const inputEnum = ['select', 'text'] - if (!inputEnum.includes(inputItem.value)) { - errorObj[`config_input_${j}`] = - `Invalid value for 'input': ${inputItem.value}, it should be one of ${inputEnum} at index: ${j}` + // Validate 'benefit' + const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit') + if ( + !benefitDelivery || + !benefitDelivery.list.some((item: any) => item.code === 'value') || + !benefitDelivery.list.some((item: any) => item.code === 'value_type') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + businessErrors[key] = + `'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + logger.error(`'benefit' tag must include 'value' and 'value_type' for offers[${offerIndex}]`) } + break + case 'exchange': + // Validate 'qualifier' + // const qualifierExchange = tags.find((tag: any) => tag.code === 'qualifier') + // if (!qualifierExchange || !qualifierExchange.list.some((item: any) => item.code === 'min_value')) { + // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]` + // businessErrors[key] = + // `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`) + // } + + // // Validate that benefits should not exist or should be empty + // const benefitExchange = tags.find((tag: any) => tag.code === 'benefit') + // if (benefitExchange && benefitExchange.list.length > 0) { + // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]` + // businessErrors[key] = + // `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}` + // logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`) + // } + if (context.domain !== 'ONDC:RET14' || context.domain !== 'ONDC:RET15') { + businessErrors['unsupportedDomain'] = + `exchange is not possible for ${context.domain} as supported domains are 'ONDC:RET14','ONDC:RET15' is required for flow: ${flow}` + } + break + case 'financing': + let collect_payment_value = collect_payment_tags?.value + if (flow === FLOW.FLOW0099 || collect_payment_value === 'no') { + const financeTagsError = validateFinanceTags(tags) + if (financeTagsError) { + let i = 0 + const len = financeTagsError.length + while (i < len) { + const key = `financeTagsError${i}` + businessErrors[key] = `${financeTagsError[i]}` + i++ + } + } + } break + default: + logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`) } }) - logger.info(`Category '${category.descriptor.name}' is valid.`) - } catch (error: any) { - logger.error(`Validation error for category '${category.descriptor.name}': ${error.message}`) } - j++ } } catch (error: any) { - logger.error(`!!Errors while checking categories in bpp/providers[${i}], ${error.stack}`) + logger.error(`Error while checking offers under bpp/providers: ${error.stack}`) } + // Checking price of items in bpp/providers try { - const fulfillments:any = onSearchCatalog['bpp/providers'][i]['fulfillments'] - fulfillments.forEach((fulfillment:any) => { - - const hasSelfPickupFulfillment = fulfillments.some((f:any) => f.type === 'Self-Pickup') - - const selfPickupTag = fulfillment.tags?.find( - (tag:any) => - tag.code === 'timing' && tag.list?.some((item:any) => item.code === 'type' && item.value === 'Self-Pickup'), - ) - - if (flow === '002') { - // Flow is 002 => Self-Pickup fulfillment is required - if (!hasSelfPickupFulfillment) { - const key = `prvdr${i}fulfillment_self_pickup_required` - errorObj[key] = `Provider[${i}] with flow=002 must have at least one fulfillment of type 'Self-Pickup'` + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any, i: number) => { + const items = provider.items + items.forEach((item: any, j: number) => { + // Skip price validation for customization items + const isCustomization = isCustomizationItem(item) + + if (!isCustomization && item.price && item.price.value) { + const priceValue = parseFloat(item.price.value) + if (priceValue < 1) { + const key = `prvdr${i}item${j}price` + businessErrors[key] = `item.price.value should be greater than 0` + } } - } else { - // For all other flows => Self-Pickup timing tag is required and must be valid - if (!selfPickupTag) { - const key = `prvdr${i}tag_self_pickup_required` - errorObj[key] = `Provider[${i}] with flow≠002 must have a 'timing' tag with list.type='Self-Pickup'` - } else { - // const timingKeys = ['day_from', 'day_to', 'time_from', 'time_to'] - const tagListMap = Object.fromEntries(selfPickupTag.list.map((t: any) => [t.code, t.value])) - const locationId = tagListMap['location'] + }) + }) + } catch (error: any) { + logger.error(`Error while checking price of items in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`) + } - if (locationId) { + // Mapping items with thier respective providers + try { + const itemProviderMap: any = {} + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any) => { + const items = provider.items + const itemArray: any = [] + items.forEach((item: any) => { + itemArray.push(item.id) + }) + itemProviderMap[provider.id] = itemArray + }) - if (!prvdrLocId.has(locationId)) { - const key = `prvdr${i}tag_timing_invalid_location` - errorObj[key] = - `'location' in Self-Pickup timing tag must match one of the provider[${i}]'s location ids` - } - } else { - const key = `prvdr${i}tag_timing_missing_location` - errorObj[key] = `'location' is missing in Self-Pickup timing tag for provider[${i}]` + setValue('itemProviderMap', itemProviderMap) + } catch (e: any) { + logger.error(`Error while mapping items with thier respective providers ${e.stack}`) + } + + // Checking for quantity of items in bpp/providers + try { + logger.info(`Checking for quantity of items in bpp/providers for /${constants.ON_SEARCH}`) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any, i: number) => { + const items = provider.items + function getProviderCreds() { + return providers.map((provider: any) => { + const creds = provider?.creds + if ((flow === FLOW.FLOW017 && creds === undefined) || creds.length === 0) { + businessErrors['MissingCreds'] = `creds must be present in order in /${constants.ON_SEARCH}` } - // Validate day_from/to are between 1 and 7 - ;['day_from', 'day_to'].forEach((code) => { - const val = parseInt(tagListMap[code]) - if (isNaN(val) || val < 1 || val > 7) { - const key = `prvdr${i}tag_timing_invalid_${code}` - errorObj[key] = `Invalid value for ${code}: ${tagListMap[code]} in Self-Pickup timing tag` - } - }) + return { + providerId: provider.id, + creds, + } + }) + } - // Validate time_from/to format - ;['time_from', 'time_to'].forEach((code) => { - const val = tagListMap[code] - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(val)) { - const key = `prvdr${i}tag_timing_invalid_${code}` - errorObj[key] = `Invalid format for ${code}: ${val} in Self-Pickup timing tag (expected HHMM)` - } - }) + const result = getProviderCreds() + setValue('credsWithProviderId', result) - // Additional validation: time_to > time_from - const tFrom = parseInt(tagListMap['time_from']) - const tTo = parseInt(tagListMap['time_to']) - if (!isNaN(tFrom) && !isNaN(tTo) && tTo <= tFrom) { - const key = `prvdr${i}tag_timing_time_order` - errorObj[key] = `'time_to' (${tTo}) must be greater than 'time_from' (${tFrom}) for Self-Pickup` + items.forEach((item: any, j: number) => { + if (item.quantity && item.quantity.available && typeof item.quantity.available.count === 'string') { + const availCount = parseInt(item.quantity.available.count, 10) + const maxCount = parseInt(item.quantity.maximum.count, 10) + const minCount = parseInt(item.quantity.minimum.count, 10) + if (!minCount) { + const key = `prvdr${i}item${j}minimum.count` + businessErrors[key] = `item.quantity.minimum.count must be added , if not set default as 99 ` + } + if (item.quantity.unitized.measure.value < 1) { + const key = `prvdr${i}item${j}unitized` + businessErrors[key] = `item.quantity.unitized.measure.value should be greater than 0` + } + if (availCount < 0 || maxCount < 0) { + const key = `prvdr${i}item${j}invldCount` + businessErrors[key] = + `item.quantity.available.count and item.quantity.maximum.count should be greater than or equal to 0` } } - } + }) }) - } catch (error:any) { - logger.error(`!!Errors while checking fulfillments in bpp/providers[${i}], ${error.stack}`) + } catch (error: any) { + logger.error(`Error while checking quantity of items in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`) + } + + // Checking for items categoryId and it's mapping with statutory_reqs + if (getValue('domain') === 'RET10') { + try { + logger.info( + `Checking for items categoryId and it's mapping with statutory_reqs in bpp/providers for /${constants.ON_SEARCH}`, + ) + const providers = onSearchCatalog['bpp/providers'] + providers.forEach((provider: any, i: number) => { + const items = provider.items + items.forEach((item: any, j: number) => { + if (!_.isEmpty(item?.category_id)) { + const statutoryRequirement: any = getStatutoryRequirement(item.category_id) + let errors: any + switch (statutoryRequirement) { + case statutory_reqs.PrepackagedFood: + errors = checkForStatutory(item, i, j, errorObj, statutory_reqs.PrepackagedFood) + break + case statutory_reqs.PackagedCommodities: + errors = checkForStatutory(item, i, j, errorObj, statutory_reqs.PackagedCommodities) + break + case statutory_reqs.None: + break + default: + const key = `prvdr${i}item${j}statutoryReq` + businessErrors[key] = + `The following item/category_id is not a valid one in bpp/providers for /${constants.ON_SEARCH}` + break + } + Object.assign(businessErrors, errors) + } + }) + }) + } catch (error: any) { + logger.error( + `Error while checking for items categoryId and it's mapping with statutory_reqs in bpp/providers for /${constants.ON_SEARCH}, ${error.stack}`, + ) + } } + // Checking for duplicate varient in bpp/providers/items for on_search + // try { + // logger.info(`Checking for duplicate varient in bpp/providers/items for on_search`) + // for (let i in onSearchCatalog['bpp/providers']) { + // const varientPath: any = findVariantPath(onSearchCatalog['bpp/providers'][i].categories) + // const items = onSearchCatalog['bpp/providers'][i].items + // const map = checkDuplicateParentIdItems(items) + // for (let key in map) { + // if (map[key].length > 1) { + // const item = varientPath.find((item: any) => { + // return item.item_id === key + // }) + // const pathForVarient = item.paths + // let valueArray = [] + // if (pathForVarient.length) { + // for (let j = 0; j < map[key].length; j++) { + // let itemValues: any = {} + // for (let path of pathForVarient) { + // if (path === 'item.quantity.unitized.measure') { + // const unit = map[key][j].quantity.unitized.measure.unit + // const value = map[key][j].quantity.unitized.measure.value + // itemValues['unit'] = unit + // itemValues['value'] = value + // } else { + // const val = findValueAtPath(path, map[key][j]) + // itemValues[path.split('.').pop()] = val + // } + // } + // valueArray.push(itemValues) + // } + // checkForDuplicates(valueArray, errorObj) + // } + // } + // } + // } + // } catch (error: any) { + // logger.error(`!!Errors while checking parent_item_id in bpp/providers/[]/items/[]/parent_item_id/, ${error.stack}`) + // } + try { - logger.info(`Checking items for provider (${prvdr.id}) in bpp/providers[${i}]`) - let j = 0 - const items = onSearchCatalog['bpp/providers'][i]['items'] - - const iLen = items.length - while (j < iLen) { - logger.info(`Validating uniqueness for item id in bpp/providers[${i}].items[${j}]...`) - const item = items[j] - - if (itemsId.has(item.id)) { - const key = `prvdr${i}item${j}` - errorObj[key] = `duplicate item id: ${item.id} in bpp/providers[${i}]` - } else { - itemsId.add(item.id) + logger.info(`Checking Providers info (bpp/providers) in /${constants.ON_SEARCH}`) + let i = 0 + const bppPrvdrs = onSearchCatalog['bpp/providers'] + const len = bppPrvdrs.length + const tmpstmp = context.timestamp + + while (i < len) { + const categoriesId = new Set() + const customGrpId = new Set() + const seqSet = new Set() + const itemCategory_id = new Set() + const categoryRankSet = new Set() + const prvdrLocationIds = new Set() + const prvdr = bppPrvdrs[i] + + logger.info(`Checking store enable/disable timestamp in bpp/providers[${i}]`) + try { + if (prvdr.time) { + const providerTime = new Date(prvdr.time.timestamp).getTime() + const contextTimestamp = new Date(tmpstmp).getTime() + if (providerTime > contextTimestamp) { + errorObj.StoreEnableDisable = `store enable/disable timestamp (/bpp/providers/time/timestamp) should be less then or equal to context.timestamp` + } + } + } catch (error: any) { + logger.error(`Error while checking store enable/disable timestamp in bpp/providers[${i}]`, error) } - if ('category_id' in item) { - itemCategory_id.add(item.category_id) - } try { - if ('category_ids' in item) { - item[`category_ids`].map((category: string, index: number) => { - const categoryId = category.split(':')[0] - const seq = category.split(':')[1] - - if (seqSet.has(seq)) { - const key = `prvdr${i}item${j}ctgryseq${index}` - errorObj[key] = `duplicate seq : ${seq} in category_ids in prvdr${i}item${j}` - } else { - seqSet.add(seq) + logger.info(`Validating media array in bpp/providers[${i}].descriptor`) + const descriptor = prvdr.descriptor + // Check if media is missing or not an array + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype is required and must be a string in media[${mediaIdx}]` } - - if (!categoriesId.has(categoryId)) { - const key = `prvdr${i}item${j}ctgryId${index}` - errorObj[key] = `item${j} should have category_ids one of the Catalog/categories/id` + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url is required and must be a string in media[${mediaIdx}]` + } + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` } }) } } catch (error: any) { - logger.error(`Error while checking category_ids for item id: ${item.id}, ${error.stack}`) + logger.error(`Error while validating media array in bpp/providers[${i}].descriptor: ${error.stack}`) } - try { - logger.info(`Checking selling price and maximum price for item id: ${item.id}`) + logger.info(`Checking store timings in bpp/providers[${i}]`) - const statutory_reqs_prepackaged_food = - onSearchCatalog['bpp/providers'][i]['items'][j]['@ondc/org/statutory_reqs_prepackaged_food'] + prvdr.locations.forEach((loc: any, iter: any) => { + try { + logger.info(`Checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`) + const has = Object.prototype.hasOwnProperty + if (has.call(loc, 'gps')) { + if (!checkGpsPrecision(loc.gps)) { + errorObj.gpsPrecision = `/bpp/providers[${i}]/locations[${iter}]/gps coordinates must be specified with at least 4 decimal places of precision.` + } + } + } catch (error) { + logger.error( + `!!Error while checking gps precision of store location in /bpp/providers[${i}]/locations[${iter}]`, + error, + ) + } - if (context.domain === 'ONDC:RET18') { - if (!statutory_reqs_prepackaged_food.ingredients_info) { - const key = `prvdr${i}items${j}@ondc/org/statutory_reqs_prepackaged_food` - errorObj[key] = - `In ONDC:RET18 for @ondc/org/statutory_reqs_prepackaged_food ingredients_info is a valid field ` + try { + if (prvdrLocId.has(loc?.id)) { + const key = `prvdr${i}${loc.id}${iter}` + businessErrors[key] = `duplicate location id: ${loc.id} in /bpp/providers[${i}]/locations[${iter}]` + } else { + prvdrLocId.add(loc.id) } - } else if (context.domain === 'ONDC:RET10') { - const mandatoryFields = [ - 'nutritional_info', - 'additives_info', - 'brand_owner_FSSAI_license_no', - 'other_FSSAI_license_no', - 'importer_FSSAI_license_no', - ] - mandatoryFields.forEach((field) => { - if (statutory_reqs_prepackaged_food && !statutory_reqs_prepackaged_food[field]) { - const key = `prvdr${i}items${j}@ondc/org/statutory_reqs_prepackaged_food` - errorObj[key] = - `In ONDC:RET10 @ondc/org/statutory_reqs_prepackaged_food following fields are valid and required 'nutritional_info', 'additives_info','other_FSSAI_license_no', - 'brand_owner_FSSAI_license_no','importer_FSSAI_license_no'` + prvdrLocationIds.add(loc?.id) + logger.info('Checking store days...') + const days = loc?.time?.days?.split(',') + days.forEach((day: any) => { + day = parseInt(day) + if (isNaN(day) || day < 1 || day > 7) { + const key = `prvdr${i}locdays${iter}` + businessErrors[key] = + `store days (bpp/providers[${i}]/locations[${iter}]/time/days) should be in the format ("1,2,3,4,5,6,7") where 1- Monday and 7- Sunday` } }) - } - } catch (error: any) { - logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${error.stack}`) - } - try { - if (item.quantity && item.quantity.maximum && typeof item.quantity.maximum.count === 'string') { - const maxCount = parseInt(item.quantity.maximum.count, 10) - const availCount = parseInt(item.quantity.available.count, 10) - if (availCount == 99 && maxCount <= 0) { - const key = `prvdr${i}item${j}maxCount` - errorObj[key] = - `item.quantity.maximum.count should be either default value 99 (no cap per order) or any other positive value (cap per order) in /bpp/providers[${i}]/items[${j}]` + logger.info('Checking fixed or split timings') + + //scenario 1: range =1 freq/times =1 + if (loc?.time?.range && (loc.time?.schedule?.frequency || loc.time?.schedule?.times)) { + const key = `prvdr${i}loctime${iter}` + businessErrors[key] = + `Either one of fixed (range) or split (frequency and times) timings should be provided in /bpp/providers[${i}]/locations[${iter}]/time` } - } - } catch (error: any) { - logger.error(`Error while checking available and max quantity for item id: ${item.id}, ${error.stack}`) - } - try { - if (item.quantity && item.quantity.maximum && typeof item.quantity.maximum.count === 'string') { - const maxCount = parseInt(item.quantity.maximum.count, 10) - const availCount = parseInt(item.quantity.available.count, 10) - if (availCount == 99 && maxCount == 0) { - const key = `prvdr${i}item${j}maxCount` - errorObj[key] = `item.quantity.maximum.count cant be 0 if item is in stock ` + // scenario 2: range=0 freq || times =1 + if (!loc?.time?.range && (!loc?.time?.schedule?.frequency || !loc?.time?.schedule?.times)) { + const key = `prvdr${i}loctime${iter}` + businessErrors[key] = + `Either one of fixed timings (range) or split timings (both frequency and times) should be provided in /bpp/providers[${i}]/locations[${iter}]/time` } - } - } catch (error: any) { - logger.error(`Error while checking available and max quantity for item id: ${item.id}, ${error.stack}`) - } - try { - if ('price' in item) { - const sPrice = parseFloat(item.price.value) - const maxPrice = parseFloat(item.price.maximum_value) - - if (sPrice > maxPrice) { - const key = `prvdr${i}item${j}Price` - errorObj[key] = - `selling price of item /price/value with id: (${item.id}) can't be greater than the maximum price /price/maximum_value in /bpp/providers[${i}]/items[${j}]/` + //scenario 3: range=1 (start and end not compliant) frequency=0; + if ('range' in loc.time) { + logger.info('checking range (fixed timings) start and end') + const startTime: any = 'start' in loc?.time?.range ? parseInt(loc?.time?.range?.start) : '' + const endTime: any = 'end' in loc?.time?.range ? parseInt(loc?.time?.range?.end) : '' + if (isNaN(startTime) || isNaN(endTime) || startTime > endTime || endTime > 2359) { + errorObj.startEndTime = `end time must be greater than start time in fixed timings /locations/time/range (fixed store timings)` + } } + } catch (error: any) { + logger.error(`Validation error for frequency: ${error.stack}`) } - } catch (error: any) { - logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${error.stack}`) - } + }) try { - logger.info(`Checking fulfillment_id for item id: ${item.id}`) + logger.info(`Checking for upcoming holidays`) + const location = onSearchCatalog['bpp/providers'][i]['locations'] + if (!location) { + logger.error('No location detected ') + } - if ('price' in item) { - const upper = parseFloat(item.price?.tags?.[0].list[1].value) - const lower = parseFloat(item.price?.tags?.[0].list[0].value) + const scheduleObject = location[i].time.schedule.holidays + const timestamp = context.timestamp + const [currentDate] = timestamp.split('T') - if (upper > lower) { - const key = `prvdr${i}item${j}Price/tags/list` - errorObj[key] = - `selling lower range of item /price/range/value with id: (${item.id}) can't be greater than the upper in /bpp/providers[${i}]/items[${j}]/` + scheduleObject.map((date: string) => { + const dateObj = new Date(date) + const currentDateObj = new Date(currentDate) + if (dateObj.getTime() < currentDateObj.getTime()) { + const key = `/message/catalog/bpp/providers/loc${i}/time/schedule/holidays` + businessErrors[key] = `Holidays cannot be past ${currentDate}` } - } - } catch (error: any) { - logger.error(`Error while checking price range for item id: ${item.id}, error: ${error.stack}`) + }) + } catch (e) { + logger.error('No Holiday', e) } try { - if (item.fulfillment_id && !onSearchFFIdsArray[i].has(item.fulfillment_id)) { - const key = `prvdr${i}item${j}ff` - errorObj[key] = - `fulfillment_id in /bpp/providers[${i}]/items[${j}] should map to one of the fulfillments id in bpp/prvdr${i}/fulfillments` + logger.info(`Checking categories for provider (${prvdr.id}) in bpp/providers[${i}]`) + let j = 0 + const categories = onSearchCatalog['bpp/providers'][i]['categories'] + if (!categories || !categories.length) { + const key = `prvdr${i}categories` + businessErrors[key] = `Support for variants is mandatory, categories must be present in bpp/providers[${i}]` } - } catch (error: any) { - logger.error(`Error while checking fulfillment_id for item id: ${item.id}, error: ${error.stack}`) - } + const iLen = categories?.length + while (j < iLen) { + logger.info(`Validating uniqueness for categories id in bpp/providers[${i}].items[${j}]...`) + const category = categories[j] + + const fulfillments = onSearchCatalog['bpp/providers'][i]['fulfillments'] + const phoneNumber = fulfillments[i].contact.phone + + if (!isValidPhoneNumber(phoneNumber)) { + const key = `bpp/providers${i}fulfillments${i}` + businessErrors[key] = + `Please enter a valid phone number consisting of 10 or 11 digits without any spaces or special characters. ` + } - try { - logger.info(`Checking location_id for item id: ${item.id}`) + if (categoriesId.has(category.id)) { + const key = `prvdr${i}category${j}` + businessErrors[key] = `duplicate category id: ${category.id} in bpp/providers[${i}]` + } else { + categoriesId.add(category.id) + } + + try { + category.tags.map((tag: { code: any; list: any[] }, index: number) => { + switch (tag.code) { + case 'attr': + const attrList = tag.list.find((item) => item.code === 'name') + if (attrList) { + const isValid = + attrList.value === 'item.quantity.unitized.measure' || + attrList.value.startsWith('item.tags.attribute') + + if (!isValid) { + const key = `prvdr${i}category${j}tags${index}` + businessErrors[key] = + `list.code == attr then name should be 'item.quantity.unitized.measure' or 'item.tags.attribute.{object.keys}' in bpp/providers[${i}]` + } + } + break + case 'type': + const codeList = tag.list.find((item) => item.code === 'type') + if ( + !( + codeList.value === 'custom_menu' || + codeList.value === 'custom_group' || + codeList.value === 'variant_group' + ) + ) { + const key = `prvdr${i}category${j}tags${index}` + businessErrors[key] = + `list.code == type then value should be one of 'custom_menu','custom_group' and 'variant_group' in bpp/providers[${i}]` + } + + if (codeList.value === 'custom_group') { + customGrpId.add(category.id) + } + + break + case 'timing': + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + errorObj.custom_menu_timing_tag = `Invalid value for '${item.code}': ${item.value}` + } + + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + errorObj.time_to = `Invalid time format for '${item.code}': ${item.value}` + } + + break + default: + errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` + } + } + + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) + + if (dayTo < dayFrom) { + errorObj.day_from = "'day_to' must be greater than or equal to 'day_from'" + } + + if (timeTo <= timeFrom) { + errorObj.time_from = "'time_to' must be greater than 'time_from'" + } + } + + break + case 'display': + for (const item of tag.list) { + if (item.code !== 'rank' || !/^-?\d+(\.\d+)?$/.test(item.value)) { + errorObj.rank = `Invalid value for 'display': ${item.value}` + } else { + if (categoryRankSet.has(category.id)) { + const key = `prvdr${i}category${j}rank` + businessErrors[key] = `duplicate rank in category id: ${category.id} in bpp/providers[${i}]` + } else { + categoryRankSet.add(category.id) + } + } + } + + break + case 'config': + const minItem: any = tag.list.find((item: { code: string }) => item.code === 'min') + const maxItem: any = tag.list.find((item: { code: string }) => item.code === 'max') + const inputItem: any = tag.list.find((item: { code: string }) => item.code === 'input') + const seqItem: any = tag.list.find((item: { code: string }) => item.code === 'seq') + + if (!minItem || !maxItem) { + businessErrors[`customization_config_${j}`] = + `Both 'min' and 'max' values are required in 'config' at index: ${j}` + } - if (item.location_id && !prvdrLocId.has(item.location_id)) { - const key = `prvdr${i}item${j}loc` - errorObj[key] = - `location_id in /bpp/providers[${i}]/items[${j}] should be one of the locations id in /bpp/providers[${i}]/locations` + if (!/^-?\d+(\.\d+)?$/.test(minItem.value)) { + businessErrors[`customization_config_min_${j}`] = + `Invalid value for ${minItem.code}: ${minItem.value} at index: ${j}` + } + + if (!/^-?\d+(\.\d+)?$/.test(maxItem.value)) { + businessErrors[`customization_config_max_${j}`] = + `Invalid value for ${maxItem.code}: ${maxItem.value}at index: ${j}` + } + + if (!/^-?\d+(\.\d+)?$/.test(seqItem.value)) { + businessErrors[`config_seq_${j}`] = `Invalid value for ${seqItem.code}: ${seqItem.value} at index: ${j}` + } + + const inputEnum = ['select', 'text'] + if (!inputEnum.includes(inputItem.value)) { + businessErrors[`config_input_${j}`] = + `Invalid value for 'input': ${inputItem.value}, it should be one of ${inputEnum} at index: ${j}` + } + + break + } + }) + logger.info(`Category '${category.descriptor.name}' is valid.`) + } catch (error: any) { + logger.error(`Validation error for category '${category.descriptor.name}': ${error.message}`) + } + + j++ } } catch (error: any) { - logger.error(`Error while checking location_id for item id: ${item.id}, error: ${error.stack}`) + logger.error(`!!Errors while checking categories in bpp/providers[${i}], ${error.stack}`) } try { - logger.info(`Checking default_selection for F&B RET11 customizations...`) + const fulfillments: any = onSearchCatalog['bpp/providers'][i]['fulfillments'] + fulfillments.forEach((fulfillment: any) => { - const items = getValue('items') + const hasSelfPickupFulfillment = fulfillments.some((f: any) => f.type === 'Self-Pickup') - _.filter(items, (item) => { - // Check if the item has customizations (tags) and a price range - if (item.customizations && item.price) { - const customTags = item.customizations.tags - const defaultSelection = customTags?.default_selection + const selfPickupTag = fulfillment.tags?.find( + (tag: any) => + tag.code === 'timing' && tag.list?.some((item: any) => item.code === 'type' && item.value === 'Self-Pickup'), + ) - const itemSellingPrice = parseFloat(item.price.value) // Selling price of the item + if (flow === '002') { + // Flow is 002 => Self-Pickup fulfillment is required + if (!hasSelfPickupFulfillment) { + const key = `prvdr${i}fulfillment_self_pickup_required` + businessErrors[key] = `Provider[${i}] with flow=002 must have at least one fulfillment of type 'Self-Pickup'` + } + } else { + // For all other flows => Self-Pickup timing tag is required and must be valid + if (!selfPickupTag) { + const key = `prvdr${i}tag_self_pickup_required` + businessErrors[key] = `Provider[${i}] with flow≠002 must have a 'timing' tag with list.type='Self-Pickup'` + } else { + // const timingKeys = ['day_from', 'day_to', 'time_from', 'time_to'] + const tagListMap = Object.fromEntries(selfPickupTag.list.map((t: any) => [t.code, t.value])) + const locationId = tagListMap['location'] - // Retrieve the customization selling price and MRP - const customizationSellingPrice = parseFloat(defaultSelection.value) - const customizationMRP = parseFloat(defaultSelection.maximum_value) + if (locationId) { - // Calculate the expected selling price and MRP for the customization + item - const expectedSellingPrice = customizationSellingPrice + itemSellingPrice - const expectedMRP = customizationMRP + itemSellingPrice + if (!prvdrLocId.has(locationId)) { + const key = `prvdr${i}tag_timing_invalid_location` + businessErrors[key] = + `'location' in Self-Pickup timing tag must match one of the provider[${i}]'s location ids` + } + } else { + const key = `prvdr${i}tag_timing_missing_location` + businessErrors[key] = `'location' is missing in Self-Pickup timing tag for provider[${i}]` + } - // Validation: Ensure that default_selection.value matches selling price of customization + item - if (defaultSelection.value !== expectedSellingPrice) { - const key = `item${item.id}CustomTags/default_selection/value` - errorObj[key] = - `The selling price of customization + item for id: ${item.id} does not match the expected value (${expectedSellingPrice}).` - } + // Validate day_from/to are between 1 and 7 + ;['day_from', 'day_to'].forEach((code) => { + const val = parseInt(tagListMap[code]) + if (isNaN(val) || val < 1 || val > 7) { + const key = `prvdr${i}tag_timing_invalid_${code}` + businessErrors[key] = `Invalid value for ${code}: ${tagListMap[code]} in Self-Pickup timing tag` + } + }) - // Validation: Ensure that default_selection.maximum_value matches MRP of customization + item - if (defaultSelection.maximum_value !== expectedMRP) { - const key = `item${item.id}CustomTags/default_selection/maximum_value` - errorObj[key] = - `The MRP of customization + item for id: ${item.id} does not match the expected MRP (${expectedMRP}).` + // Validate time_from/to format + ;['time_from', 'time_to'].forEach((code) => { + const val = tagListMap[code] + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(val)) { + const key = `prvdr${i}tag_timing_invalid_${code}` + businessErrors[key] = `Invalid format for ${code}: ${val} in Self-Pickup timing tag (expected HHMM)` + } + }) + + // Additional validation: time_to > time_from + const tFrom = parseInt(tagListMap['time_from']) + const tTo = parseInt(tagListMap['time_to']) + if (!isNaN(tFrom) && !isNaN(tTo) && tTo <= tFrom) { + const key = `prvdr${i}tag_timing_time_order` + businessErrors[key] = `'time_to' (${tTo}) must be greater than 'time_from' (${tFrom}) for Self-Pickup` + } } - - logger.info(`Checked default_selection for item id: ${item.id}`) } }) } catch (error: any) { - logger.error(`Error while checking default_selection for items, ${error.stack}`) + logger.error(`!!Errors while checking fulfillments in bpp/providers[${i}], ${error.stack}`) } try { - logger.info(`Checking consumer care details for item id: ${item.id}`) - if ('@ondc/org/contact_details_consumer_care' in item) { - let consCare = item['@ondc/org/contact_details_consumer_care'] - - consCare = consCare.split(',') - - if (!isValidPhoneNumber(consCare[2])) { - const key = `prvdr${i}consCare` - errorObj[key] = - `@ondc/org/contact_details_consumer_care contactno should consist of 10 or 11 digits without any spaces or special characters in /bpp/providers[${i}]/items` + logger.info(`Checking items for provider (${prvdr.id}) in bpp/providers[${i}]`) + let j = 0 + const items = onSearchCatalog['bpp/providers'][i]['items'] + + const iLen = items.length + while (j < iLen) { + logger.info(`Validating uniqueness for item id in bpp/providers[${i}].items[${j}]...`) + const item = items[j] + + //Validating media array in bpp/providers[${i}].items[${j}] + try { + logger.info(`Validating media array in bpp/providers[${i}].items[${j}].descriptor`) + const descriptor = item.descriptor + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype is required and must be a string in media[${mediaIdx}]` + } + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url is required and must be a string in media[${mediaIdx}]` + } + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + businessErrors[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + businessErrors[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` + } + }) + } + } catch (error: any) { + logger.error(`Error while validating media array in bpp/providers[${i}].items[${j}].descriptor: ${error.stack}`) } - if (consCare.length < 3) { - const key = `prvdr${i}consCare` - errorObj[key] = - `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` + if (itemsId.has(item.id)) { + const key = `prvdr${i}item${j}` + businessErrors[key] = `duplicate item id: ${item.id} in bpp/providers[${i}]` } else { - const checkEmail: boolean = emailRegex(consCare[1].trim()) - if (isNaN(consCare[2].trim()) || !checkEmail) { - const key = `prvdr${i}consCare` - errorObj[key] = - `@ondc/org/contact_details_consumer_care email should be in /bpp/providers[${i}]/items` - } + itemsId.add(item.id) } - } - } catch (error: any) { - logger.error(`Error while checking consumer care details for item id: ${item.id}, ${error.stack}`) - } - try { - item.tags.map((tag: { code: any; list: any[] }, index: number) => { - switch (tag.code) { - case 'type': - if ( - tag.list && - Array.isArray(tag.list) && - tag.list.some( - (listItem: { code: string; value: string }) => - listItem.code === 'type' && listItem.value === 'item', - ) - ) { - if (!item.time) { - const key = `prvdr${i}item${j}time` - errorObj[key] = `item_id: ${item.id} should contain time object in bpp/providers[${i}]` + if ('category_id' in item) { + itemCategory_id.add(item.category_id) + } + try { + if ('category_ids' in item) { + item[`category_ids`].map((category: string, index: number) => { + const categoryId = category.split(':')[0] + const seq = category.split(':')[1] + + if (seqSet.has(seq)) { + const key = `prvdr${i}item${j}ctgryseq${index}` + businessErrors[key] = `duplicate seq : ${seq} in category_ids in prvdr${i}item${j}` + } else { + seqSet.add(seq) } - } - - break - case 'custom_group': - tag.list.map((it: { code: string; value: string }, index: number) => { - if (!customGrpId.has(it.value)) { - const key = `prvdr${i}item${j}tag${index}cstmgrp_id` - errorObj[key] = - `item_id: ${item.id} should have custom_group_id one of the ids passed in categories bpp/providers[${i}]` + if (!categoriesId.has(categoryId)) { + const key = `prvdr${i}item${j}ctgryId${index}` + businessErrors[key] = `item${j} should have category_ids one of the Catalog/categories/id` } }) + } + } catch (error: any) { + logger.error(`Error while checking category_ids for item id: ${item.id}, ${error.stack}`) + } - break + try { + logger.info(`Checking selling price and maximum price for item id: ${item.id}`) - case 'config': - const idList: any = tag.list.find((item: { code: string }) => item.code === 'id') - const minList: any = tag.list.find((item: { code: string }) => item.code === 'min') - const maxList: any = tag.list.find((item: { code: string }) => item.code === 'max') - const seqList: any = tag.list.find((item: { code: string }) => item.code === 'seq') + const statutory_reqs_prepackaged_food = + onSearchCatalog['bpp/providers'][i]['items'][j]['@ondc/org/statutory_reqs_prepackaged_food'] + + if (context.domain === 'ONDC:RET18') { + if (!statutory_reqs_prepackaged_food.ingredients_info) { + const key = `prvdr${i}items${j}@ondc/org/statutory_reqs_prepackaged_food` + businessErrors[key] = + `In ONDC:RET18 for @ondc/org/statutory_reqs_prepackaged_food ingredients_info is a valid field ` + } + } else if (context.domain === 'ONDC:RET10') { + const mandatoryFields = [ + 'nutritional_info', + 'additives_info', + 'brand_owner_FSSAI_license_no', + 'other_FSSAI_license_no', + 'importer_FSSAI_license_no', + ] + mandatoryFields.forEach((field) => { + if (statutory_reqs_prepackaged_food && !statutory_reqs_prepackaged_food[field]) { + const key = `prvdr${i}items${j}@ondc/org/statutory_reqs_prepackaged_food` + businessErrors[key] = + `In ONDC:RET10 @ondc/org/statutory_reqs_prepackaged_food following fields are valid and required 'nutritional_info', 'additives_info','other_FSSAI_license_no', + 'brand_owner_FSSAI_license_no','importer_FSSAI_license_no'` + } + }) + } + } catch (error: any) { + logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${error.stack}`) + } - if (!categoriesId.has(idList.value)) { - const key = `prvdr${i}item${j}tags${index}config_list` - errorObj[key] = - `value in catalog/items${j}/tags${index}/config/list/ should be one of the catalog/category/ids` + try { + if (item.quantity && item.quantity.maximum && typeof item.quantity.maximum.count === 'string') { + const maxCount = parseInt(item.quantity.maximum.count, 10) + const availCount = parseInt(item.quantity.available.count, 10) + if (availCount == 99 && maxCount <= 0) { + const key = `prvdr${i}item${j}maxCount` + businessErrors[key] = + `item.quantity.maximum.count should be either default value 99 (no cap per order) or any other positive value (cap per order) in /bpp/providers[${i}]/items[${j}]` } + } + } catch (error: any) { + logger.error(`Error while checking available and max quantity for item id: ${item.id}, ${error.stack}`) + } - if (!/^-?\d+(\.\d+)?$/.test(minList.value)) { - const key = `prvdr${i}item${j}tags${index}config_min` - errorObj[key] = `Invalid value for ${minList.code}: ${minList.value}` + try { + if (item.quantity && item.quantity.maximum && typeof item.quantity.maximum.count === 'string') { + const maxCount = parseInt(item.quantity.maximum.count, 10) + const availCount = parseInt(item.quantity.available.count, 10) + if (availCount == 99 && maxCount == 0) { + const key = `prvdr${i}item${j}maxCount` + businessErrors[key] = `item.quantity.maximum.count cant be 0 if item is in stock ` } + } + } catch (error: any) { + logger.error(`Error while checking available and max quantity for item id: ${item.id}, ${error.stack}`) + } + + try { + if ('price' in item) { + const sPrice = parseFloat(item.price.value) + const maxPrice = parseFloat(item.price.maximum_value) - if (!/^-?\d+(\.\d+)?$/.test(maxList.value)) { - const key = `prvdr${i}item${j}tags${index}config_max` - errorObj[key] = `Invalid value for ${maxList.code}: ${maxList.value}` + if (sPrice > maxPrice) { + const key = `prvdr${i}item${j}Price` + businessErrors[key] = + `selling price of item /price/value with id: (${item.id}) can't be greater than the maximum price /price/maximum_value in /bpp/providers[${i}]/items[${j}]/` } + } + } catch (error: any) { + logger.error(`Error while checking selling price and maximum price for item id: ${item.id}, ${error.stack}`) + } - if (!/^-?\d+(\.\d+)?$/.test(seqList.value)) { - const key = `prvdr${i}item${j}tags${index}config_seq` - errorObj[key] = `Invalid value for ${seqList.code}: ${seqList.value}` + try { + logger.info(`Checking fulfillment_id for item id: ${item.id}`) + + if ('price' in item) { + const upper = parseFloat(item.price?.tags?.[0].list[1].value) + const lower = parseFloat(item.price?.tags?.[0].list[0].value) + + if (upper > lower) { + const key = `prvdr${i}item${j}Price/tags/list` + businessErrors[key] = + `selling lower range of item /price/range/value with id: (${item.id}) can't be greater than the upper in /bpp/providers[${i}]/items[${j}]/` } + } + } catch (error: any) { + logger.error(`Error while checking price range for item id: ${item.id}, error: ${error.stack}`) + } - break + try { + if (item.fulfillment_id && !onSearchFFIdsArray[i].has(item.fulfillment_id)) { + const key = `prvdr${i}item${j}ff` + businessErrors[key] = + `fulfillment_id in /bpp/providers[${i}]/items[${j}] should map to one of the fulfillments id in bpp/prvdr${i}/fulfillments` + } + } catch (error: any) { + logger.error(`Error while checking fulfillment_id for item id: ${item.id}, error: ${error.stack}`) + } - case 'timing': - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 5 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - const key = `prvdr${i}item${j}tags${index}timing_day` - errorObj[key] = `Invalid value for '${item.code}': ${item.value}` - } + try { + logger.info(`Checking location_id for item id: ${item.id}`) - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - const key = `prvdr${i}item${j}tags${index}timing_time` - errorObj[key] = `Invalid time format for '${item.code}': ${item.value}` - } + if (item.location_id && !prvdrLocId.has(item.location_id)) { + const key = `prvdr${i}item${j}loc` + businessErrors[key] = + `location_id in /bpp/providers[${i}]/items[${j}] should be one of the locations id in /bpp/providers[${i}]/locations` + } + } catch (error: any) { + logger.error(`Error while checking location_id for item id: ${item.id}, error: ${error.stack}`) + } + try { + logger.info(`Checking default_selection for F&B RET11 customizations...`) - break - default: - errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` - } - } + const items = getValue('items') + + _.filter(items, (item) => { + // Check if the item has customizations (tags) and a price range + if (item.customizations && item.price) { + const customTags = item.customizations.tags + const defaultSelection = customTags?.default_selection - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + const itemSellingPrice = parseFloat(item.price.value) // Selling price of the item - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) + // Retrieve the customization selling price and MRP + const customizationSellingPrice = parseFloat(defaultSelection.value) + const customizationMRP = parseFloat(defaultSelection.maximum_value) - if (dayTo < dayFrom) { - const key = `prvdr${i}item${j}tags${index}timing_dayfrom` - errorObj[key] = "'day_to' must be greater than or equal to 'day_from'" + // Calculate the expected selling price and MRP for the customization + item + const expectedSellingPrice = customizationSellingPrice + itemSellingPrice + const expectedMRP = customizationMRP + itemSellingPrice + + // Validation: Ensure that default_selection.value matches selling price of customization + item + if (defaultSelection.value !== expectedSellingPrice) { + const key = `item${item.id}CustomTags/default_selection/value` + businessErrors[key] = + `The selling price of customization + item for id: ${item.id} does not match the expected value (${expectedSellingPrice}).` } - if (timeTo <= timeFrom) { - const key = `prvdr${i}item${j}tags${index}timing_timefrom` - errorObj[key] = "'time_to' must be greater than 'time_from'" + // Validation: Ensure that default_selection.maximum_value matches MRP of customization + item + if (defaultSelection.maximum_value !== expectedMRP) { + const key = `item${item.id}CustomTags/default_selection/maximum_value` + businessErrors[key] = + `The MRP of customization + item for id: ${item.id} does not match the expected MRP (${expectedMRP}).` } + + logger.info(`Checked default_selection for item id: ${item.id}`) } + }) + } catch (error: any) { + logger.error(`Error while checking default_selection for items, ${error.stack}`) + } - break + try { + logger.info(`Checking consumer care details for item id: ${item.id}`) + if ('@ondc/org/contact_details_consumer_care' in item) { + let consCare = item['@ondc/org/contact_details_consumer_care'] - case 'veg_nonveg': - const allowedCodes = ['veg', 'non_veg', 'egg'] + consCare = consCare.split(',') - for (const it of tag.list) { - if (it.code && !allowedCodes.includes(it.code)) { - const key = `prvdr${i}item${j}tag${index}veg_nonveg` - errorObj[key] = - `item_id: ${item.id} should have veg_nonveg one of the 'veg', 'non_veg' in bpp/providers[${i}]` - } + if (!isValidPhoneNumber(consCare[2])) { + const key = `prvdr${i}consCare` + businessErrors[key] = + `@ondc/org/contact_details_consumer_care contactno should consist of 10 or 11 digits without any spaces or special characters in /bpp/providers[${i}]/items` } - break + if (consCare.length < 3) { + const key = `prvdr${i}consCare` + businessErrors[key] = + `@ondc/org/contact_details_consumer_care should be in the format "name,email,contactno" in /bpp/providers[${i}]/items` + } else { + const checkEmail: boolean = emailRegex(consCare[1].trim()) + if (isNaN(consCare[2].trim()) || !checkEmail) { + const key = `prvdr${i}consCare` + businessErrors[key] = + `@ondc/org/contact_details_consumer_care email should be in /bpp/providers[${i}]/items` + } + } + } + } catch (error: any) { + logger.error(`Error while checking consumer care details for item id: ${item.id}, ${error.stack}`) } - }) - } catch (error: any) { - logger.error(`Error while checking tags for item id: ${item.id}`, error) - } - j++ - } - } catch (error: any) { - logger.error(`!!Errors while checking items in bpp/providers[${i}], ${error.stack}`) - } + try { + item.tags.map((tag: { code: any; list: any[] }, index: number) => { + switch (tag.code) { + case 'type': + if ( + tag.list && + Array.isArray(tag.list) && + tag.list.some( + (listItem: { code: string; value: string }) => + listItem.code === 'type' && listItem.value === 'item', + ) + ) { + if (!item.time) { + const key = `prvdr${i}item${j}time` + businessErrors[key] = `item_id: ${item.id} should contain time object in bpp/providers[${i}]` + } + } - try { - logger.info(`checking rank in bpp/providers[${i}].category.tags`) - const rankSeq = isSequenceValid(seqSet) - if (rankSeq === false) { - const key = `prvdr${i}ctgry_tags` - errorObj[key] = `rank should be in sequence provided in bpp/providers[${i}]/categories/tags/display` - } - } catch (error: any) { - logger.error(`!!Errors while checking rank in bpp/providers[${i}].category.tags, ${error.stack}`) - } + break - // servicability Construct - try { - logger.info(`Checking serviceability construct for bpp/providers[${i}]`) - const tags = onSearchCatalog['bpp/providers'][i]['tags'] - if (!tags || !tags.length) { - const key = `prvdr${i}tags` - errorObj[key] = `tags must be present in bpp/providers[${i}]` - } + case 'custom_group': + tag.list.map((it: { code: string; value: string }, index: number) => { + if (!customGrpId.has(it.value)) { + const key = `prvdr${i}item${j}tag${index}cstmgrp_id` + businessErrors[key] = + `item_id: ${item.id} should have custom_group_id one of the ids passed in categories bpp/providers[${i}]` + } + }) - if (tags) { - const circleRequired = checkServiceabilityType(tags) - if (circleRequired) { - const errors = validateLocations(message.catalog['bpp/providers'][i].locations, tags) - errorObj = { ...errorObj, ...errors } - } - } + break - //checking for each serviceability construct and matching serviceability constructs with the previous ones - const serviceabilitySet = new Set() - const timingSet = new Set() - tags.forEach((sc: any, t: any) => { - if (sc.code === 'serviceability') { - if (serviceabilitySet.has(JSON.stringify(sc))) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous serviceability constructs` - } + case 'config': + const idList: any = tag.list.find((item: { code: string }) => item.code === 'id') + const minList: any = tag.list.find((item: { code: string }) => item.code === 'min') + const maxList: any = tag.list.find((item: { code: string }) => item.code === 'max') + const seqList: any = tag.list.find((item: { code: string }) => item.code === 'seq') - serviceabilitySet.add(JSON.stringify(sc)) - if ('list' in sc) { - if (sc.list.length < 5) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract` - } + if (!categoriesId.has(idList.value)) { + const key = `prvdr${i}item${j}tags${index}config_list` + businessErrors[key] = + `value in catalog/items${j}/tags${index}/config/list/ should be one of the catalog/category/ids` + } - //checking location - const loc = sc.list.find((elem: any) => elem && elem.code === 'location') - if (!loc) { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` - } else if (typeof loc === 'object' && 'value' in loc) { - if (!prvdrLocId.has(loc.value)) { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `location in serviceability construct should be one of the location ids bpp/providers[${i}]/locations` - } - } else { - const key = `prvdr${i}tags${t}loc` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` - } + if (!/^-?\d+(\.\d+)?$/.test(minList.value)) { + const key = `prvdr${i}item${j}tags${index}config_min` + businessErrors[key] = `Invalid value for ${minList.code}: ${minList.value}` + } - //checking category - const ctgry = sc.list.find((elem: any) => elem && elem.code === 'category') - if (!ctgry) { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` - } else if (typeof ctgry === 'object' && 'value' in ctgry) { - if (!itemCategory_id.has(ctgry.value)) { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `category in serviceability construct should be one of the category ids bpp/providers[${i}]/items/category_id` - } - } else { - const key = `prvdr${i}tags${t}ctgry` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` - } + if (!/^-?\d+(\.\d+)?$/.test(maxList.value)) { + const key = `prvdr${i}item${j}tags${index}config_max` + businessErrors[key] = `Invalid value for ${maxList.code}: ${maxList.value}` + } - //checking type (hyperlocal, intercity or PAN India) - const type = sc.list.find((elem: any) => elem && elem.code === 'type') - if (!type) { - const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` - } else if (typeof type === 'object' && 'value' in type) { - switch (type.value) { - case '10': - { - //For hyperlocal - - //checking value - const val = sc.list.find((elem: any) => elem && elem.code === 'val') - if (!val) { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } else if (typeof val === 'object' && 'value' in val) { - if (isNaN(val.value)) { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be a number (code:"val") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` - } - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + if (!/^-?\d+(\.\d+)?$/.test(seqList.value)) { + const key = `prvdr${i}item${j}tags${index}config_seq` + businessErrors[key] = `Invalid value for ${seqList.code}: ${seqList.value}` } - //checking unit - const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') - if (!unit) { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } else if (typeof unit === 'object' && 'value' in unit) { - if (unit.value != 'km') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "km" (code:"unit") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` + break + + case 'timing': + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 5 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + const key = `prvdr${i}item${j}tags${index}timing_day` + businessErrors[key] = `Invalid value for '${item.code}': ${item.value}` + } + + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + const key = `prvdr${i}item${j}tags${index}timing_time` + businessErrors[key] = `Invalid time format for '${item.code}': ${item.value}` + } + + break + default: + errorObj.Tagtiming = `Invalid list.code for 'timing': ${item.code}` } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` } - } - break - case '11': - { - //intercity - - //checking value - const val = sc.list.find((elem: any) => elem && elem.code === 'val') - if (!val) { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } else if (typeof val === 'object' && 'value' in val) { - const pincodes = val.value.split(/,|-/) - pincodes.forEach((pincode: any) => { - if (isNaN(pincode) || pincode.length != 6) { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be a valid range of pincodes (code:"val") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` - } - }) - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) - //checking unit - const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') - if (!unit) { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } else if (typeof unit === 'object' && 'value' in unit) { - if (unit.value != 'pincode') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "pincode" (code:"unit") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` + if (dayTo < dayFrom) { + const key = `prvdr${i}item${j}tags${index}timing_dayfrom` + businessErrors[key] = "'day_to' must be greater than or equal to 'day_from'" } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } - } - break - case '12': - { - //PAN India - - //checking value - const val = sc.list.find((elem: any) => elem && elem.code === 'val') - if (!val) { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` - } else if (typeof val === 'object' && 'value' in val) { - if (val.value != 'IND') { - const key = `prvdr${i}tags${t}valvalue` - errorObj[key] = - `value should be "IND" (code:"val") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + if (timeTo <= timeFrom) { + const key = `prvdr${i}item${j}tags${index}timing_timefrom` + businessErrors[key] = "'time_to' must be greater than 'time_from'" } - } else { - const key = `prvdr${i}tags${t}val` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` } - //checking unit - const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') - if (!unit) { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` - } else if (typeof unit === 'object' && 'value' in unit) { - if (unit.value != 'country') { - const key = `prvdr${i}tags${t}unitvalue` - errorObj[key] = - `value should be "country" (code:"unit") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + break + + case 'veg_nonveg': + const allowedCodes = ['veg', 'non_veg', 'egg'] + + for (const it of tag.list) { + if (it.code && !allowedCodes.includes(it.code)) { + const key = `prvdr${i}item${j}tag${index}veg_nonveg` + businessErrors[key] = + `item_id: ${item.id} should have veg_nonveg one of the 'veg', 'non_veg' in bpp/providers[${i}]` } - } else { - const key = `prvdr${i}tags${t}unit` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` } - } - break - default: { - const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (invalid type "${type.value}")` + break } - } - } else { - const key = `prvdr${i}tags${t}type` - errorObj[key] = - `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` + }) + } catch (error: any) { + logger.error(`Error while checking tags for item id: ${item.id}`, error) } + + j++ } + } catch (error: any) { + logger.error(`!!Errors while checking items in bpp/providers[${i}], ${error.stack}`) } - if (sc.code === 'timing') { - if (timingSet.has(JSON.stringify(sc))) { - const key = `prvdr${i}tags${t}` - errorObj[key] = - `timing construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous timing constructs` + + try { + logger.info(`checking rank in bpp/providers[${i}].category.tags`) + const rankSeq = isSequenceValid(seqSet) + if (rankSeq === false) { + const key = `prvdr${i}ctgry_tags` + businessErrors[key] = `rank should be in sequence provided in bpp/providers[${i}]/categories/tags/display` } + } catch (error: any) { + logger.error(`!!Errors while checking rank in bpp/providers[${i}].category.tags, ${error.stack}`) + } - timingSet.add(JSON.stringify(sc)) - const fulfillments = prvdr['fulfillments'] - const fulfillmentTypes = fulfillments.map((fulfillment: any) => fulfillment.type) - - let isOrderPresent = false - const typeCode = sc?.list.find((item: any) => item.code === 'type') - if (typeCode) { - const timingType = typeCode.value - if ( - timingType === 'Order' || - timingType === 'Delivery' || - timingType === 'Self-Pickup' || - timingType === 'All' - ) { - isOrderPresent = true - } else if (!fulfillmentTypes.includes(timingType)) { - errorObj[`provider[${i}].timing`] = - `The type '${timingType}' in timing tags should match with types in fulfillments array, along with 'Order'` - } + // servicability Construct + try { + logger.info(`Checking serviceability construct for bpp/providers[${i}]`) + const tags = onSearchCatalog['bpp/providers'][i]['tags'] + if (!tags || !tags.length) { + const key = `prvdr${i}tags` + businessErrors[key] = `tags must be present in bpp/providers[${i}]` } - if (!isOrderPresent) { - errorObj[`provider[${i}].tags.timing`] = `'Order' type must be present in timing tags` + if (tags) { + const circleRequired = checkServiceabilityType(tags) + if (circleRequired) { + const errors = validateLocations(message.catalog['bpp/providers'][i].locations, tags) + Object.assign(businessErrors, errors) + } } - } - }) - if (isEmpty(serviceabilitySet)) { - const key = `prvdr${i}tags/serviceability` - errorObj[key] = `serviceability construct is mandatory in /bpp/providers[${i}]/tags` - } else if (serviceabilitySet.size != itemCategory_id.size) { - const key = `prvdr${i}/serviceability` - errorObj[key] = - `The number of unique category_id should be equal to count of serviceability in /bpp/providers[${i}]` - } - if (isEmpty(timingSet)) { - const key = `prvdr${i}tags/timing` - errorObj[key] = `timing construct is mandatory in /bpp/providers[${i}]/tags` - } else { - const timingsPayloadArr = new Array(...timingSet).map((item: any) => JSON.parse(item)) - const timingsAll = _.chain(timingsPayloadArr) - .filter((payload) => _.some(payload.list, { code: 'type', value: 'All' })) - .value() - - // Getting timings object for 'Delivery', 'Self-Pickup' and 'Order' - const timingsOther = _.chain(timingsPayloadArr) - .filter( - (payload) => - _.some(payload.list, { code: 'type', value: 'Order' }) || - _.some(payload.list, { code: 'type', value: 'Delivery' }) || - _.some(payload.list, { code: 'type', value: 'Self-Pickup' }), - ) - .value() - if (timingsAll.length > 0 && timingsOther.length > 0) { - errorObj[`prvdr${i}tags/timing`] = - `If the timings of type 'All' is provided then timings construct for 'Order'/'Delivery'/'Self-Pickup' is not required` - } + //checking for each serviceability construct and matching serviceability constructs with the previous ones + const serviceabilitySet = new Set() + const timingSet = new Set() + tags.forEach((sc: any, t: any) => { + if (sc.code === 'serviceability') { + if (serviceabilitySet.has(JSON.stringify(sc))) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous serviceability constructs` + } - const arrTimingTypes = new Set() - - function checkTimingTag(tag: any) { - const typeObject = tag.list.find((item: { code: string }) => item.code === 'type') - const typeValue = typeObject ? typeObject.value : null - arrTimingTypes.add(typeValue) - for (const item of tag.list) { - switch (item.code) { - case 'day_from': - case 'day_to': - const dayValue = parseInt(item.value) - if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { - errorObj[`prvdr${i}/day_to$/${typeValue}`] = `Invalid value for '${item.code}': ${item.value}` + serviceabilitySet.add(JSON.stringify(sc)) + if ('list' in sc) { + if (sc.list.length < 5) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract` } - break - case 'time_from': - case 'time_to': - if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { - errorObj[`prvdr${i}/tags/time_to/${typeValue}`] = - `Invalid time format for '${item.code}': ${item.value}` + //checking location + const loc = sc.list.find((elem: any) => elem && elem.code === 'location') + if (!loc) { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` + } else if (typeof loc === 'object' && 'value' in loc) { + if (!prvdrLocId.has(loc.value)) { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `location in serviceability construct should be one of the location ids bpp/providers[${i}]/locations` + } + } else { + const key = `prvdr${i}tags${t}loc` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (location is missing)` } - break - case 'location': - if (!prvdrLocationIds.has(item.value)) { - errorObj[`prvdr${i}/tags/location/${typeValue}`] = - `Invalid location value as it's unavailable in location/ids` + + //checking category + const ctgry = sc.list.find((elem: any) => elem && elem.code === 'category') + if (!ctgry) { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` + } else if (typeof ctgry === 'object' && 'value' in ctgry) { + if (!itemCategory_id.has(ctgry.value)) { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `category in serviceability construct should be one of the category ids bpp/providers[${i}]/items/category_id` + } + } else { + const key = `prvdr${i}tags${t}ctgry` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (category is missing)` } - break - case 'type': - break - default: - errorObj[`prvdr${i}/tags/tag_timings/${typeValue}`] = `Invalid list.code for 'timing': ${item.code}` - } - } - const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') - const dayToItem = tag.list.find((item: any) => item.code === 'day_to') - const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') - const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + //checking type (hyperlocal, intercity or PAN India) + const type = sc.list.find((elem: any) => elem && elem.code === 'type') + if (!type) { + const key = `prvdr${i}tags${t}type` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` + } else if (typeof type === 'object' && 'value' in type) { + switch (type.value) { + case '10': + { + //For hyperlocal + + //checking value + const val = sc.list.find((elem: any) => elem && elem.code === 'val') + if (!val) { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } else if (typeof val === 'object' && 'value' in val) { + if (isNaN(val.value)) { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be a number (code:"val") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } + + //checking unit + const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') + if (!unit) { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } else if (typeof unit === 'object' && 'value' in unit) { + if (unit.value != 'km') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "km" (code:"unit") for type 10 (hyperlocal) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } + } + + break + case '11': + { + //intercity + + //checking value + const val = sc.list.find((elem: any) => elem && elem.code === 'val') + if (!val) { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } else if (typeof val === 'object' && 'value' in val) { + const pincodes = val.value.split(/,|-/) + pincodes.forEach((pincode: any) => { + if (isNaN(pincode) || pincode.length != 6) { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be a valid range of pincodes (code:"val") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` + } + }) + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } + + //checking unit + const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') + if (!unit) { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } else if (typeof unit === 'object' && 'value' in unit) { + if (unit.value != 'pincode') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "pincode" (code:"unit") for type 11 (intercity) in /bpp/providers[${i}]/tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } + } + + break + case '12': + { + //PAN India + + //checking value + const val = sc.list.find((elem: any) => elem && elem.code === 'val') + if (!val) { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } else if (typeof val === 'object' && 'value' in val) { + if (val.value != 'IND') { + const key = `prvdr${i}tags${t}valvalue` + businessErrors[key] = + `value should be "IND" (code:"val") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}val` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "val")` + } - if (dayFromItem && dayToItem && timeFromItem && timeToItem) { - const dayFrom = parseInt(dayFromItem.value, 10) - const dayTo = parseInt(dayToItem.value, 10) - const timeFrom = parseInt(timeFromItem.value, 10) - const timeTo = parseInt(timeToItem.value, 10) + //checking unit + const unit = sc.list.find((elem: any) => elem && elem.code === 'unit') + if (!unit) { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } else if (typeof unit === 'object' && 'value' in unit) { + if (unit.value != 'country') { + const key = `prvdr${i}tags${t}unitvalue` + businessErrors[key] = + `value should be "country" (code:"unit") for type 12 (PAN India) in /bpp/providers[${i}]tags[${t}]` + } + } else { + const key = `prvdr${i}tags${t}unit` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (value is missing for code "unit")` + } + } - if (dayTo < dayFrom) { - errorObj[`prvdr${i}/tags/day_from/${typeValue}`] = - "'day_to' must be greater than or equal to 'day_from'" + break + default: { + const key = `prvdr${i}tags${t}type` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (invalid type "${type.value}")` + } + } + } else { + const key = `prvdr${i}tags${t}type` + businessErrors[key] = + `serviceability construct /bpp/providers[${i}]/tags[${t}] should be defined as per the API contract (type is missing)` + } + } } + if (sc.code === 'timing') { + if (timingSet.has(JSON.stringify(sc))) { + const key = `prvdr${i}tags${t}` + businessErrors[key] = + `timing construct /bpp/providers[${i}]/tags[${t}] should not be same with the previous timing constructs` + } + + timingSet.add(JSON.stringify(sc)) + const fulfillments = prvdr['fulfillments'] + const fulfillmentTypes = fulfillments.map((fulfillment: any) => fulfillment.type) + + let isOrderPresent = false + const typeCode = sc?.list.find((item: any) => item.code === 'type') + if (typeCode) { + const timingType = typeCode.value + if ( + timingType === 'Order' || + timingType === 'Delivery' || + timingType === 'Self-Pickup' || + timingType === 'All' + ) { + isOrderPresent = true + } else if (!fulfillmentTypes.includes(timingType)) { + businessErrors[`provider[${i}].timing`] = + `The type '${timingType}' in timing tags should match with types in fulfillments array, along with 'Order'` + } + } - if (timeTo <= timeFrom) { - errorObj[`prvdr${i}/tags/time_from/${typeValue}`] = "'time_to' must be greater than 'time_from'" + if (!isOrderPresent) { + businessErrors[`provider[${i}].tags.timing`] = `'Order' type must be present in timing tags` + } } + }) + if (isEmpty(serviceabilitySet)) { + const key = `prvdr${i}tags/serviceability` + businessErrors[key] = `serviceability construct is mandatory in /bpp/providers[${i}]/tags` + } else if (serviceabilitySet.size != itemCategory_id.size) { + const key = `prvdr${i}/serviceability` + businessErrors[key] = + `The number of unique category_id should be equal to count of serviceability in /bpp/providers[${i}]` } - } + if (isEmpty(timingSet)) { + const key = `prvdr${i}tags/timing` + businessErrors[key] = `timing construct is mandatory in /bpp/providers[${i}]/tags` + } else { + const timingsPayloadArr = new Array(...timingSet).map((item: any) => JSON.parse(item)) + const timingsAll = _.chain(timingsPayloadArr) + .filter((payload) => _.some(payload.list, { code: 'type', value: 'All' })) + .value() + + // Getting timings object for 'Delivery', 'Self-Pickup' and 'Order' + const timingsOther = _.chain(timingsPayloadArr) + .filter( + (payload) => + _.some(payload.list, { code: 'type', value: 'Order' }) || + _.some(payload.list, { code: 'type', value: 'Delivery' }) || + _.some(payload.list, { code: 'type', value: 'Self-Pickup' }), + ) + .value() - if (timingsAll.length > 0) { - if (timingsAll.length > 1) { - errorObj[`prvdr${i}tags/timing/All`] = `The timings object for 'All' should be provided once!` - } - checkTimingTag(timingsAll[0]) - } + if (timingsAll.length > 0 && timingsOther.length > 0) { + businessErrors[`prvdr${i}tags/timing`] = + `If the timings of type 'All' is provided then timings construct for 'Order'/'Delivery'/'Self-Pickup' is not required` + } - if (timingsOther.length > 0) { - timingsOther.forEach((tagTimings: any) => { - checkTimingTag(tagTimings) - }) - onSearchFFTypeSet.forEach((type: any) => { - if (!arrTimingTypes.has(type)) { - errorObj[`prvdr${i}/tags/timing/${type}`] = `The timings object must be present for ${type} in the tags` + const arrTimingTypes = new Set() + + function checkTimingTag(tag: any) { + const typeObject = tag.list.find((item: { code: string }) => item.code === 'type') + const typeValue = typeObject ? typeObject.value : null + arrTimingTypes.add(typeValue) + for (const item of tag.list) { + switch (item.code) { + case 'day_from': + case 'day_to': + const dayValue = parseInt(item.value) + if (isNaN(dayValue) || dayValue < 1 || dayValue > 7 || !/^-?\d+(\.\d+)?$/.test(item.value)) { + businessErrors[`prvdr${i}/day_to$/${typeValue}`] = `Invalid value for '${item.code}': ${item.value}` + } + + break + case 'time_from': + case 'time_to': + if (!/^([01]\d|2[0-3])[0-5]\d$/.test(item.value)) { + businessErrors[`prvdr${i}/tags/time_to/${typeValue}`] = + `Invalid time format for '${item.code}': ${item.value}` + } + break + case 'location': + if (!prvdrLocationIds.has(item.value)) { + businessErrors[`prvdr${i}/tags/location/${typeValue}`] = + `Invalid location value as it's unavailable in location/ids` + } + break + case 'type': + break + default: + businessErrors[`prvdr${i}/tags/tag_timings/${typeValue}`] = `Invalid list.code for 'timing': ${item.code}` + } + } + + const dayFromItem = tag.list.find((item: any) => item.code === 'day_from') + const dayToItem = tag.list.find((item: any) => item.code === 'day_to') + const timeFromItem = tag.list.find((item: any) => item.code === 'time_from') + const timeToItem = tag.list.find((item: any) => item.code === 'time_to') + + if (dayFromItem && dayToItem && timeFromItem && timeToItem) { + const dayFrom = parseInt(dayFromItem.value, 10) + const dayTo = parseInt(dayToItem.value, 10) + const timeFrom = parseInt(timeFromItem.value, 10) + const timeTo = parseInt(timeToItem.value, 10) + + if (dayTo < dayFrom) { + businessErrors[`prvdr${i}/tags/day_from/${typeValue}`] = + "'day_to' must be greater than or equal to 'day_from'" + } + + if (timeTo <= timeFrom) { + businessErrors[`prvdr${i}/tags/time_from/${typeValue}`] = "'time_to' must be greater than 'time_from'" + } + } } - }) - arrTimingTypes.forEach((type: any) => { - if (type != 'Order' && type != 'All' && !onSearchFFTypeSet.has(type)) { - errorObj[`prvdr${i}/tags/timing/${type}`] = - `The timings object ${type} is not present in the onSearch fulfillments` + + if (timingsAll.length > 0) { + if (timingsAll.length > 1) { + businessErrors[`prvdr${i}tags/timing/All`] = `The timings object for 'All' should be provided once!` + } + checkTimingTag(timingsAll[0]) + } + + if (timingsOther.length > 0) { + timingsOther.forEach((tagTimings: any) => { + checkTimingTag(tagTimings) + }) + onSearchFFTypeSet.forEach((type: any) => { + if (!arrTimingTypes.has(type)) { + businessErrors[`prvdr${i}/tags/timing/${type}`] = `The timings object must be present for ${type} in the tags` + } + }) + arrTimingTypes.forEach((type: any) => { + if (type != 'Order' && type != 'All' && !onSearchFFTypeSet.has(type)) { + businessErrors[`prvdr${i}/tags/timing/${type}`] = + `The timings object ${type} is not present in the onSearch fulfillments` + } + }) + if (!arrTimingTypes.has('Order')) { + businessErrors[`prvdr${i}/tags/timing/order`] = `The timings object must be present for Order in the tags` + } } - }) - if (!arrTimingTypes.has('Order')) { - errorObj[`prvdr${i}/tags/timing/order`] = `The timings object must be present for Order in the tags` } + } catch (error: any) { + logger.error(`!!Error while checking serviceability construct for bpp / providers[${i}], ${error.stack} `) } - } - } catch (error: any) { - logger.error(`!!Error while checking serviceability construct for bpp / providers[${i}], ${error.stack} `) - } - try { - logger.info( - `Checking if catalog_link type in message / catalog / bpp / providers[${i}]/tags[1]/list[0] is link or inline`, - ) - const tags = bppPrvdrs[i].tags + try { + logger.info( + `Checking if catalog_link type in message / catalog / bpp / providers[${i}]/tags[1]/list[0] is link or inline`, + ) + const tags = bppPrvdrs[i].tags - let list: any = [] - tags.map((data: any) => { - if (data.code == 'catalog_link') { - list = data.list - } - }) + let list: any = [] + tags.map((data: any) => { + if (data.code == 'catalog_link') { + list = data.list + } + }) - list.map((data: any) => { - if (data.code === 'type') { - if (data.value === 'link') { - if (bppPrvdrs[0].items) { - errorObj[`message / catalog / bpp / providers[0]`] = - `Items arrays should not be present in message / catalog / bpp / providers[${i}]` + list.map((data: any) => { + if (data.code === 'type') { + if (data.value === 'link') { + if (bppPrvdrs[0].items) { + businessErrors[`message / catalog / bpp / providers[0]`] = + `Items arrays should not be present in message / catalog / bpp / providers[${i}]` + } + } } - } + }) + } catch (error: any) { + logger.error(`Error while checking the type of catalog_link`) } - }) - } catch (error: any) { - logger.error(`Error while checking the type of catalog_link`) - } - i++ - } + i++ + } - setValue(`${ApiSequence.ON_SEARCH}prvdrLocId`, prvdrLocId) - setValue(`${ApiSequence.ON_SEARCH}itemsId`, itemsId) + setValue(`${ApiSequence.ON_SEARCH}prvdrLocId`, prvdrLocId) + setValue(`${ApiSequence.ON_SEARCH}itemsId`, itemsId) - if (flow === FLOW.FLOW012) { - const providers = data.message.catalog['bpp/providers'] + if (flow === FLOW.FLOW012) { + const providers = data.message.catalog['bpp/providers'] - providers.forEach((provider: any) => { - const codItems = provider.items.filter((item: any) => item?.['@ondc/org/available_on_cod'] === true) - setValue('coditems', codItems) - }) - } - if (flow === FLOW.FLOW01F) { - const descriptor = data.message.catalog['bpp/descriptor']?.tags - const termsObj = descriptor.find((ele: { code: string }): any => ele.code === 'bpp_terms') - setValue('bppTerms', termsObj.list) - } - if (flow === FLOW.FLOW016) { - // Flow 016 specific validation - check for custom group in categories - try { - logger.info(`Checking for custom group categories in /${constants.ON_SEARCH} for flow 016`) - const providers = message.catalog['bpp/providers'] - const customGroups: any[] = [] - - if (!providers || !Array.isArray(providers) || providers.length === 0) { - errorObj.missingProviders = `No providers found in /${constants.ON_SEARCH} for flow 016` - } else { - let customGroupFound = false - - for (const provider of providers) { - if (provider.categories && Array.isArray(provider.categories)) { - for (const category of provider.categories) { - // Check if the category has the required structure for custom_group - const isTypeCustomGroup = category.tags?.some( - (tag: any) => - tag.code === 'type' && - tag.list?.some((item: any) => item.code === 'type' && item.value === 'custom_group'), - ) + providers.forEach((provider: any) => { + const codItems = provider.items.filter((item: any) => item?.['@ondc/org/available_on_cod'] === true) + setValue('coditems', codItems) + }) + } + if (flow === FLOW.FLOW01F) { + const descriptor = data.message.catalog['bpp/descriptor']?.tags + const termsObj = descriptor.find((ele: { code: string }): any => ele.code === 'bpp_terms') + setValue('bppTerms', termsObj.list) + } + if (flow === FLOW.FLOW016) { + // Flow 016 specific validation - check for custom group in categories + try { + logger.info(`Checking for custom group categories in /${constants.ON_SEARCH} for flow 016`) + const providers = message.catalog['bpp/providers'] + const customGroups: any[] = [] - const hasConfigInput = category.tags?.some( - (tag: any) => - tag.code === 'config' && - tag.list?.some((item: any) => item.code === 'input' && item.value === 'text'), - ) + if (!providers || !Array.isArray(providers) || providers.length === 0) { + errorObj.missingProviders = `No providers found in /${constants.ON_SEARCH} for flow 016` + } else { + let customGroupFound = false + + for (const provider of providers) { + if (provider.categories && Array.isArray(provider.categories)) { + for (const category of provider.categories) { + // Check if the category has the required structure for custom_group + const isTypeCustomGroup = category.tags?.some( + (tag: any) => + tag.code === 'type' && + tag.list?.some((item: any) => item.code === 'type' && item.value === 'custom_group'), + ) - if (isTypeCustomGroup && hasConfigInput) { - customGroupFound = true - // Save the custom group for later validation - customGroups.push({ - id: category.id, - descriptor: category.descriptor, - tags: category.tags, - }) - logger.info(`Found valid custom group category: ${category.id}`) + const hasConfigInput = category.tags?.some( + (tag: any) => + tag.code === 'config' && + tag.list?.some((item: any) => item.code === 'input' && item.value === 'text'), + ) + + if (isTypeCustomGroup && hasConfigInput) { + customGroupFound = true + // Save the custom group for later validation + customGroups.push({ + id: category.id, + descriptor: category.descriptor, + tags: category.tags, + }) + logger.info(`Found valid custom group category: ${category.id}`) + } + } } } - } - } - if (!customGroupFound) { - errorObj.missingCustomGroup = `No custom_group category with input type 'text' found in /${constants.ON_SEARCH} for flow 016` - } else { - // Save custom groups to DAO for later validation in SELECT - setValue('flow016_custom_groups', customGroups) - logger.info(`Saved ${customGroups.length} custom groups for flow 016`) + if (!customGroupFound) { + errorObj.missingCustomGroup = `No custom_group category with input type 'text' found in /${constants.ON_SEARCH} for flow 016` + } else { + // Save custom groups to DAO for later validation in SELECT + setValue('flow016_custom_groups', customGroups) + logger.info(`Saved ${customGroups.length} custom groups for flow 016`) + } + } + } catch (error: any) { + logger.error( + `Error while checking custom group categories in /${constants.ON_SEARCH} for flow 016, ${error.stack}`, + ) + errorObj.customGroupCheckError = `Error validating custom group categories: ${error.message}` } } } catch (error: any) { - logger.error( - `Error while checking custom group categories in /${constants.ON_SEARCH} for flow 016, ${error.stack}`, - ) - errorObj.customGroupCheckError = `Error validating custom group categories: ${error.message}` + logger.error(`!!Error while checking Providers info in /${constants.ON_SEARCH}, ${error.stack}`) } } + + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors } + } catch (error: any) { - logger.error(`!!Error while checking Providers info in /${constants.ON_SEARCH}, ${error.stack}`) + logger.error( + `Error while checking for JSON structure and required fields for ${ApiSequence.ON_SEARCH} API with schemaValidation: ${schemaValidation}`, + ) + return { + error: 'error while checking on_search api', + } } - - return Object.keys(errorObj).length > 0 && errorObj } diff --git a/utils/Retail_.1.2.5/Search/search.ts b/utils/Retail_.1.2.5/Search/search.ts index cf96ccd1..fbdd1665 100644 --- a/utils/Retail_.1.2.5/Search/search.ts +++ b/utils/Retail_.1.2.5/Search/search.ts @@ -13,14 +13,23 @@ import { import _ from 'lodash' import { FLOW } from '../../enum' -export const checkSearch = (data: any, msgIdSet: any, flow: any) => { +export const checkSearch = ( + data: any, + msgIdSet: any, + flow: any, + stateless: boolean = false, + schemaValidation?: boolean, +) => { const errorObj: any = {} + const schemaErrors: any = {} + const businessErrors: any = {} + try { - logger.info(`Checking JSON structure and required fields for ${ApiSequence.SEARCH} API`) + logger.info(`Checking JSON structure and required fields for ${ApiSequence.SEARCH} API with schemaValidation: ${schemaValidation}`) if (!data || isObjectEmpty(data)) { errorObj[ApiSequence.SEARCH] = 'JSON cannot be empty' - return + return errorObj } if ( @@ -34,138 +43,154 @@ export const checkSearch = (data: any, msgIdSet: any, flow: any) => { return Object.keys(errorObj).length > 0 && errorObj } - const schemaValidation = validateSchemaRetailV2(data.context.domain.split(':')[1], constants.SEARCH, data) + // Schema validation - run unless schemaValidation is explicitly false + if (schemaValidation !== false) { + const schemaDomain = data.context.domain.split(':')[1] + logger.info(`checkSearch: calling schema validation with domain=${schemaDomain}, api=${constants.SEARCH}`) + const schemaValidation = validateSchemaRetailV2(schemaDomain, constants.SEARCH, data) - if (schemaValidation !== 'error') { - Object.assign(errorObj, schemaValidation) - } - - try { - logger.info(`Adding Message Id /${constants.SEARCH}`) - msgIdSet.add(data.context.message_id) - setValue(`${ApiSequence.SEARCH}_msgId`, data.context.message_id) - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.SEARCH}, ${error.stack}`) - } - - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (schemaValidation !== 'error') { + Object.assign(schemaErrors, schemaValidation) + } } - try { - logger.info(`Checking for context in /context for ${constants.SEARCH} API`) - const contextRes: any = checkContext(data.context, constants.SEARCH) - setValue(`${ApiSequence.SEARCH}_context`, data.context) + // Business logic validation - run unless schemaValidation is explicitly true + if (schemaValidation !== true) { + try { + logger.info(`Adding Message Id /${constants.SEARCH}`) + msgIdSet.add(data.context.message_id) + setValue(`${ApiSequence.SEARCH}_msgId`, data.context.message_id) + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.SEARCH}, ${error.stack}`) + } - if (!contextRes?.valid) { - Object.assign(errorObj, contextRes.ERRORS) + if (!stateless && !_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + businessErrors[`Domain[${data.context.action}]`] = `Domain should be same in each action` } - } catch (error: any) { - logger.error(`Error in checking context for ${ApiSequence.SEARCH}: ${error.stack}`) - } - try { - logger.info(`Checking for buyer app finder fee amount for ${ApiSequence.SEARCH}`) - const buyerFF = parseFloat(data.message.intent?.payment?.['@ondc/org/buyer_app_finder_fee_amount']) + try { + logger.info(`Checking for context in /context for ${constants.SEARCH} API`) + const contextRes: any = checkContext(data.context, constants.SEARCH) + setValue(`${ApiSequence.SEARCH}_context`, data.context) - if (!isNaN(buyerFF)) { - setValue(`${ApiSequence.SEARCH}_buyerFF`, buyerFF) - } else { - errorObj['payment'] = 'payment should have a key @ondc/org/buyer_app_finder_fee_amount' + if (!contextRes?.valid) { + Object.assign(businessErrors, contextRes.ERRORS) + } + } catch (error: any) { + logger.error(`Error in checking context for ${ApiSequence.SEARCH}: ${error.stack}`) } - } catch (error: any) { - logger.error(`Error in checking buyer app finder fee amount: ${error.stack}`) - } - try { - logger.info(`Checking for fulfillment/end/location/gps for ${ApiSequence.SEARCH}`) - const fulfillment = data.message.intent && data.message.intent?.fulfillment - if (fulfillment && fulfillment.end) { - const gps = fulfillment.end?.location?.gps - if (gps) { - if (!checkGpsPrecision(gps)) { - errorObj['gpsPrecision'] = - 'fulfillment/end/location/gps coordinates must be specified with at least 4 decimal places of precision.' - } + try { + logger.info(`Checking for buyer app finder fee amount for ${ApiSequence.SEARCH}`) + const buyerFF = parseFloat(data.message.intent?.payment?.['@ondc/org/buyer_app_finder_fee_amount']) + + if (!isNaN(buyerFF)) { + setValue(`${ApiSequence.SEARCH}_buyerFF`, buyerFF) } else { - errorObj['fulfillmentLocation'] = 'fulfillment/end/location should have a required property gps' + businessErrors['payment'] = 'payment should have a key @ondc/org/buyer_app_finder_fee_amount' } + } catch (error: any) { + logger.error(`Error in checking buyer app finder fee amount: ${error.stack}`) } - } catch (error: any) { - logger.error(`Error in checking fulfillment/end/location/gps: ${error.stack}`) - } - try { - logger.info(`Checking for item and category in /message/intent for ${constants.SEARCH} API`) - if (hasProperty(data.message.intent, 'item') && hasProperty(data.message.intent, 'category')) { - if (!errorObj.intent) { - errorObj.intent = {} + try { + logger.info(`Checking for fulfillment/end/location/gps for ${ApiSequence.SEARCH}`) + const fulfillment = data.message.intent && data.message.intent?.fulfillment + if (fulfillment && fulfillment.end) { + const gps = fulfillment.end?.location?.gps + if (gps) { + if (!checkGpsPrecision(gps)) { + businessErrors['gpsPrecision'] = + 'fulfillment/end/location/gps coordinates must be specified with at least 4 decimal places of precision.' + } + } else { + businessErrors['gps'] = 'fulfillment/end/location/gps is required' + } } - errorObj.intent.category_or_item = '/message/intent cannot have both properties item and category' + } catch (error: any) { + logger.error(`Error in checking fulfillment/end/location/gps: ${error.stack}`) } - } catch (error: any) { - logger.error(`Error in checking item and category in /message/intent: ${error.stack}`) - } - - try { - logger.info(`Checking for tags in /message/intent for ${constants.SEARCH} API`) - if (data.message.intent?.tags) { - const bap_terms = data.message.intent.tags.find((tag: any) => tag.code === "bap_terms") - const termsError = validateTermsList(bap_terms?.list ?? []); - if (!termsError.isValid) { - errorObj['bap_terms'] = termsError.errors; + try { + logger.info(`Checking for item and category in /message/intent for ${constants.SEARCH} API`) + if (hasProperty(data.message.intent, 'item') && hasProperty(data.message.intent, 'category')) { + if (!businessErrors.intent) { + businessErrors.intent = {} + } + businessErrors.intent.category_or_item = '/message/intent cannot have both properties item and category' } - if (flow === FLOW.FLOW025) { - const tags = data.message.intent?.tags + } catch (error: any) { + logger.error(`Error in checking item and category in /message/intent: ${error.stack}`) + } - const bnpTag = tags?.find((tag: any) => tag.code === 'bnp_demand_signal') - const searchTerm = bnpTag.list?.find((item: any) => item.code === 'search_term') + try { + logger.info(`Checking for tags in /message/intent for ${constants.SEARCH} API`) + if (data.message.intent?.tags) { + const bap_terms = data.message.intent.tags.find((tag: any) => tag.code === "bap_terms") + const termsError = validateTermsList(bap_terms?.list ?? []); - if (!bnpTag) { - errorObj['missingTags'] = `Missing tag with code 'bnp_demand_signal' in /${constants.SEARCH} for flow025` + if (!termsError.isValid) { + businessErrors['bap_terms'] = termsError.errors; } + + if (flow === FLOW.FLOW025) { + const tags = data.message.intent?.tags + const bnpTag = tags?.find((tag: any) => tag.code === 'bnp_demand_signal') + const searchTerm = bnpTag?.list?.find((item: any) => item.code === 'search_term') + + if (!bnpTag) { + businessErrors['missingTags'] = `Missing tag with code 'bnp_demand_signal' in /${constants.SEARCH} for flow025` + } - if (!searchTerm) { - errorObj['missingSearchTerm'] = `'bnp_demand_signal' tag is present but missing 'search_term' in its list` - } - } - if (flow === FLOW.FLOW022) { - const tags = data.message.intent?.tags - const bnpTag = tags?.find((tag: any) => tag.code === 'bap_promos') - if (!bnpTag) { - errorObj['missingTags'] = `Missing tag with code 'bap_promos' in /${constants.SEARCH} for flow022` - } - const category = bnpTag.list?.find((item: any) => item.code === 'category') - const hasFrom = bnpTag.list?.find((item: any) => item.code === 'from') - const hasTo = bnpTag.list?.find((item: any) => item.code === 'to') - if (!category) { - errorObj['missingSearchTerm'] = `'bnp_demand_signal' tag is present but missing 'category' in its list` - } - if (!hasFrom || !hasTo) { - errorObj['missingSearchTerm'] = - `'bnp_demand_signal' tag is present but missing timing indicators such as ' from, to ' in its list` + if (!searchTerm) { + businessErrors['missingSearchTerm'] = `'bnp_demand_signal' tag is present but missing 'search_term' in its list` + } } - if (hasFrom && hasTo) { - const fromDate = new Date(hasFrom.value) - const toDate = new Date(hasTo.value) - - if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { - errorObj['invalidDate'] = `'from' or 'to' contains an invalid date format.` - } else if (fromDate > toDate) { - errorObj['invalidDateRange'] = `'from' date cannot be later than 'to' date.` + + if (flow === FLOW.FLOW022) { + const tags = data.message.intent?.tags + const bnpTag = tags?.find((tag: any) => tag.code === 'bap_promos') + if (!bnpTag) { + businessErrors['missingTags'] = `Missing tag with code 'bap_promos' in /${constants.SEARCH} for flow022` + } + const category = bnpTag?.list?.find((item: any) => item.code === 'category') + const hasFrom = bnpTag?.list?.find((item: any) => item.code === 'from') + const hasTo = bnpTag?.list?.find((item: any) => item.code === 'to') + if (!category) { + businessErrors['missingSearchTerm'] = `'bnp_demand_signal' tag is present but missing 'category' in its list` + } + if (!hasFrom || !hasTo) { + businessErrors['missingSearchTerm'] = + `'bnp_demand_signal' tag is present but missing timing indicators such as ' from, to ' in its list` + } + if (hasFrom && hasTo) { + const fromDate = new Date(hasFrom.value) + const toDate = new Date(hasTo.value) + + if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { + businessErrors['invalidDate'] = `'from' or 'to' contains an invalid date format.` + } else if (fromDate > toDate) { + businessErrors['invalidDateRange'] = `'from' date cannot be later than 'to' date.` + } } } - } - const tagErrors = checkTagConditions(data.message, data.context, ApiSequence.SEARCH) - tagErrors?.length ? (errorObj.intent = { ...errorObj.intent, tags: tagErrors }) : null + const tagErrors = checkTagConditions(data.message, data.context, ApiSequence.SEARCH) + if (tagErrors?.length) { + businessErrors.intent = { ...businessErrors.intent, tags: tagErrors } + } + } + } catch (error: any) { + logger.error(`Error in checking tags in /message/intent: ${error.stack}`) } - } catch (error: any) { - logger.error(`Error in checking tags in /message/intent: ${error.stack}`) } - return Object.keys(errorObj).length > 0 && errorObj + // Always return buckets; let caller decide which to surface + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors } + } catch (error: any) { logger.error( `Error while checking for JSON structure and required fields for ${ApiSequence.SEARCH}: ${error.stack}`, diff --git a/utils/Retail_.1.2.5/SearchInc/onSearchIncremental.ts b/utils/Retail_.1.2.5/SearchInc/onSearchIncremental.ts index f6708abd..36d308a3 100644 --- a/utils/Retail_.1.2.5/SearchInc/onSearchIncremental.ts +++ b/utils/Retail_.1.2.5/SearchInc/onSearchIncremental.ts @@ -10,7 +10,10 @@ import { DOMAIN } from '../../enum' import { validateSchemaRetailV2, isObjectEmpty, checkContext, checkGpsPrecision, emailRegex, checkMandatoryTags } from '../..' import _, { isEmpty } from 'lodash' -export const checkOnsearchIncremental = (data: any, msgIdSet: any) => { +export const checkOnsearchIncremental = (data: any, msgIdSet: any, schemaValidation?: boolean, stateless?: boolean) => { + const errorObj: any = {} + const schemaErrors: any = {} + if (!data || isObjectEmpty(data)) { return { [ApiSequence.INC_ONSEARCH]: 'JSON cannot be empty' } } @@ -20,12 +23,18 @@ export const checkOnsearchIncremental = (data: any, msgIdSet: any) => { return { missingFields: '/context, /message, /catalog or /message/catalog is missing or empty' } } - const schemaValidation = validateSchemaRetailV2('RET11', 'on_search_inc', data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2('RET11', 'on_search_inc', data) + : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } const contextRes: any = checkContext(context, constants.ON_SEARCH) setValue(`${ApiSequence.INC_ONSEARCH}_context`, context) - const errorObj: any = {} try { logger.info(`Adding Message Id of /${constants.ON_SEARCHINC}`) if (getValue(`${ApiSequence.INC_SEARCH}_push`)) { @@ -38,10 +47,10 @@ export const checkOnsearchIncremental = (data: any, msgIdSet: any) => { logger.error(`!!Error while checking message id for /${constants.ON_SEARCHINC}, ${error.stack}`) } - if (schemaValidation !== 'error') { + if (schemaValidationResult !== 'error') { Object.assign(errorObj, schemaValidation) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + if (!stateless && !_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` } @@ -101,209 +110,209 @@ export const checkOnsearchIncremental = (data: any, msgIdSet: any) => { // ) // } const onSearchIncrementakCatalog: any = message.catalog -//Validating Offers -try { - logger.info(`Checking offers.tags under bpp/providers`); - - // Iterate through bpp/providers - for (let i in onSearchIncrementakCatalog['bpp/providers']) { - const offers = onSearchIncrementakCatalog['bpp/providers'][i]?.offers ?? null; - if (!offers) { - offers.forEach((offer: any, offerIndex: number) => { - const tags = offer.tags; - - // Ensure tags exist - if (!tags || !Array.isArray(tags)) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags`; - errorObj[key] = `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`; - logger.error(`Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`); - return; - } + //Validating Offers + try { + logger.info(`Checking offers.tags under bpp/providers`); + + // Iterate through bpp/providers + for (let i in onSearchIncrementakCatalog['bpp/providers']) { + const offers = onSearchIncrementakCatalog['bpp/providers'][i]?.offers ?? null; + if (!offers) { + offers.forEach((offer: any, offerIndex: number) => { + const tags = offer.tags; + + // Ensure tags exist + if (!tags || !Array.isArray(tags)) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags`; + errorObj[key] = `Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`; + logger.error(`Tags must be provided for offers[${offerIndex}] with descriptor code '${offer.descriptor?.code}'`); + return; + } - // Validate based on offer type - switch (offer.descriptor?.code) { - case 'discount': - // Validate 'qualifier' - const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier'); - if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - } + // Validate based on offer type + switch (offer.descriptor?.code) { + case 'discount': + // Validate 'qualifier' + const qualifierDiscount = tags.find((tag: any) => tag.code === 'qualifier'); + if (!qualifierDiscount || !qualifierDiscount.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + } - // Validate 'benefit' - const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit'); - if ( - !benefitDiscount || - !benefitDiscount.list.some((item: any) => item.code === 'value') || - !benefitDiscount.list.some((item: any) => item.code === 'value_type') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}]`); - } - else { - // check to ensure that the value of discount must be -ve - const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value'); - if (valueItem && parseFloat(valueItem.value) >= 0) { + // Validate 'benefit' + const benefitDiscount = tags.find((tag: any) => tag.code === 'benefit'); + if ( + !benefitDiscount || + !benefitDiscount.list.some((item: any) => item.code === 'value') || + !benefitDiscount.list.some((item: any) => item.code === 'value_type') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include both 'value' and 'value_type' for offers[${offerIndex}]`); + } + else { + // check to ensure that the value of discount must be -ve + const valueItem = benefitDiscount.list.find((item: any) => item.code === 'value'); + if (valueItem && parseFloat(valueItem.value) >= 0) { const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]/value`; errorObj[key] = `'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; logger.error(`'value' in 'benefit' tag must be a negative amount for offers[${offerIndex}]`); + } } - } - - break; - - case 'buyXgetY': - // Validate 'qualifier' - const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier'); - if ( - !qualifierBuyXgetY || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || - !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`); - } - // Validate 'benefit' - const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit'); - if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`); - } - break; - - case 'freebie': - // Validate 'qualifier' - const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier'); - if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - } + break; + + case 'buyXgetY': + // Validate 'qualifier' + const qualifierBuyXgetY = tags.find((tag: any) => tag.code === 'qualifier'); + if ( + !qualifierBuyXgetY || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'min_value') || + !qualifierBuyXgetY.list.some((item: any) => item.code === 'item_count') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' and 'item_count' for offers[${offerIndex}]`); + } - // Validate 'benefit' - const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit'); - if ( - !benefitFreebie || - !benefitFreebie.list.some((item: any) => item.code === 'item_count') || - !benefitFreebie.list.some((item: any) => item.code === 'item_id') || - !benefitFreebie.list.some((item: any) => item.code === 'item_value') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`); - } - break; - - case 'slab': - // Validate 'qualifier' - const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier'); - if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - } + // Validate 'benefit' + const benefitBuyXgetY = tags.find((tag: any) => tag.code === 'benefit'); + if (!benefitBuyXgetY || !benefitBuyXgetY.list.some((item: any) => item.code === 'item_count')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include 'item_count' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include 'item_count' for offers[${offerIndex}]`); + } + break; + + case 'freebie': + // Validate 'qualifier' + const qualifierFreebie = tags.find((tag: any) => tag.code === 'qualifier'); + if (!qualifierFreebie || !qualifierFreebie.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + } - // Validate 'benefit' - const benefitSlab = tags.find((tag: any) => tag.code === 'benefit'); - if ( - !benefitSlab || - !benefitSlab.list.some((item: any) => item.code === 'value') || - !benefitSlab.list.some((item: any) => item.code === 'value_type') || - !benefitSlab.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); - } - break; - - case 'combo': - // Validate 'qualifier' - const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier'); - if ( - !qualifierCombo || - !qualifierCombo.list.some((item: any) => item.code === 'min_value') || - !qualifierCombo.list.some((item: any) => item.code === 'item_id') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`); - } + // Validate 'benefit' + const benefitFreebie = tags.find((tag: any) => tag.code === 'benefit'); + if ( + !benefitFreebie || + !benefitFreebie.list.some((item: any) => item.code === 'item_count') || + !benefitFreebie.list.some((item: any) => item.code === 'item_id') || + !benefitFreebie.list.some((item: any) => item.code === 'item_value') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include 'item_count', 'item_id', and 'item_value' for offers[${offerIndex}]`); + } + break; + + case 'slab': + // Validate 'qualifier' + const qualifierSlab = tags.find((tag: any) => tag.code === 'qualifier'); + if (!qualifierSlab || !qualifierSlab.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + } - // Validate 'benefit' - const benefitCombo = tags.find((tag: any) => tag.code === 'benefit'); - if ( - !benefitCombo || - !benefitCombo.list.some((item: any) => item.code === 'value') || - !benefitCombo.list.some((item: any) => item.code === 'value_type') || - !benefitCombo.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); - } - break; - - case 'delivery': - // Validate 'qualifier' - const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier'); - if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - } - - - // Validate 'benefit' - const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit'); - if ( - !benefitDelivery || - !benefitDelivery.list.some((item: any) => item.code === 'value') || - !benefitDelivery.list.some((item: any) => item.code === 'value_type') || - !benefitDelivery.list.some((item: any) => item.code === 'value_cap') - ) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); - } - break; - - // case 'exchange': - // case 'financing': - // // Validate 'qualifier' - // const qualifierExchangeFinancing = tags.find((tag: any) => tag.code === 'qualifier'); - // if (!qualifierExchangeFinancing || !qualifierExchangeFinancing.list.some((item: any) => item.code === 'min_value')) { - // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; - // errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); - // } - - // Validate that benefits should not exist or should be empty - const benefitExchangeFinancing = tags.find((tag: any) => tag.code === 'benefit'); - if (benefitExchangeFinancing && benefitExchangeFinancing.list.length > 0) { - const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; - errorObj[key] = `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; - logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`); - } - break; + // Validate 'benefit' + const benefitSlab = tags.find((tag: any) => tag.code === 'benefit'); + if ( + !benefitSlab || + !benefitSlab.list.some((item: any) => item.code === 'value') || + !benefitSlab.list.some((item: any) => item.code === 'value_type') || + !benefitSlab.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); + } + break; + + case 'combo': + // Validate 'qualifier' + const qualifierCombo = tags.find((tag: any) => tag.code === 'qualifier'); + if ( + !qualifierCombo || + !qualifierCombo.list.some((item: any) => item.code === 'min_value') || + !qualifierCombo.list.some((item: any) => item.code === 'item_id') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' and 'item_id' for offers[${offerIndex}]`); + } + + // Validate 'benefit' + const benefitCombo = tags.find((tag: any) => tag.code === 'benefit'); + if ( + !benefitCombo || + !benefitCombo.list.some((item: any) => item.code === 'value') || + !benefitCombo.list.some((item: any) => item.code === 'value_type') || + !benefitCombo.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); + } + break; + + case 'delivery': + // Validate 'qualifier' + const qualifierDelivery = tags.find((tag: any) => tag.code === 'qualifier'); + if (!qualifierDelivery || !qualifierDelivery.list.some((item: any) => item.code === 'min_value')) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + } - // No validation for benefits as it is not required - break; - default: - logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`); - } - }); + // Validate 'benefit' + const benefitDelivery = tags.find((tag: any) => tag.code === 'benefit'); + if ( + !benefitDelivery || + !benefitDelivery.list.some((item: any) => item.code === 'value') || + !benefitDelivery.list.some((item: any) => item.code === 'value_type') || + !benefitDelivery.list.some((item: any) => item.code === 'value_cap') + ) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must include 'value', 'value_type', and 'value_cap' for offers[${offerIndex}]`); + } + break; + + // case 'exchange': + // case 'financing': + // // Validate 'qualifier' + // const qualifierExchangeFinancing = tags.find((tag: any) => tag.code === 'qualifier'); + // if (!qualifierExchangeFinancing || !qualifierExchangeFinancing.list.some((item: any) => item.code === 'min_value')) { + // const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[qualifier]`; + // errorObj[key] = `'qualifier' tag must include 'min_value' for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + // logger.error(`'qualifier' tag must include 'min_value' for offers[${offerIndex}]`); + // } + + // Validate that benefits should not exist or should be empty + const benefitExchangeFinancing = tags.find((tag: any) => tag.code === 'benefit'); + if (benefitExchangeFinancing && benefitExchangeFinancing.list.length > 0) { + const key = `bpp/providers[${i}]/offers[${offerIndex}]/tags[benefit]`; + errorObj[key] = `'benefit' tag must not include any values for offers[${offerIndex}] when offer.descriptor.code = ${offer.descriptor.code}`; + logger.error(`'benefit' tag must not include any values for offers[${offerIndex}]`); + } + break; + + // No validation for benefits as it is not required + break; + + default: + logger.info(`No specific validation required for offer type: ${offer.descriptor?.code}`); + } + }); + } } + } catch (error: any) { + logger.error(`Error while checking offers.tags under bpp/providers: ${error.stack}`); } -} catch (error: any) { - logger.error(`Error while checking offers.tags under bpp/providers: ${error.stack}`); -} @@ -406,6 +415,34 @@ try { logger.info(`Validating uniqueness for provider id in bpp/providers[${i}]...`) const prvdr = bppPrvdrs[i] + try { + logger.info(`Validating media array in bpp/providers[${i}].descriptor`) + const descriptor = prvdr.descriptor + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + errorObj[key] = `mimetype is required and must be a string in media[${mediaIdx}]` + } + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + errorObj[key] = `url is required and must be a string in media[${mediaIdx}]` + } + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/mimetype` + errorObj[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/descriptor/media[${mediaIdx}]/url` + errorObj[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` + } + }) + } + } catch (error: any) { + logger.error(`Error while validating media array in bpp/providers[${i}].descriptor: ${error.stack}`) + } + if (onSearchPrvdrIDS && !onSearchPrvdrIDS.includes(prvdr.id)) { const key = `prvdr${i}id` errorObj[key] = @@ -522,6 +559,33 @@ try { while (j < iLen) { logger.info(`Validating uniqueness for item id in bpp/providers[${i}].items[${j}]...`) const item = items[j] + try { + logger.info(`Validating media array in bpp/providers[${i}].items[${j}].descriptor`) + const descriptor = item.descriptor + if (descriptor && Array.isArray(descriptor.media)) { + descriptor.media.forEach((mediaObj: any, mediaIdx: number) => { + if (!mediaObj.mimetype || typeof mediaObj.mimetype !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + errorObj[key] = `mimetype is required and must be a string in media[${mediaIdx}]` + } + if (!mediaObj.url || typeof mediaObj.url !== 'string') { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + errorObj[key] = `url is required and must be a string in media[${mediaIdx}]` + } + const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/mpeg'] + if (!allowedMimeTypes.includes(mediaObj.mimetype)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/mimetype` + errorObj[key] = `mimetype must be one of ${allowedMimeTypes.join(', ')}` + } + if (!/^https?:\/\/.+/.test(mediaObj.url)) { + const key = `bpp/providers[${i}]/items[${j}]/descriptor/media[${mediaIdx}]/url` + errorObj[key] = `url must be a valid http(s) URL in media[${mediaIdx}]` + } + }) + } + } catch (error: any) { + logger.error(`Error while validating media array in bpp/providers[${i}].items[${j}].descriptor: ${error.stack}`) + } if (itemsFullRefresh.includes(item?.id)) { const itemFullRefresh = prvdrFullRefresh.items.find((obj: any) => obj.id === item.id) @@ -764,7 +828,27 @@ try { logger.error(`!!Error while checking Providers info in /${constants.ON_SEARCH}, ${error.stack}`) } - return Object.keys(errorObj).length > 0 && errorObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + + if (stateless) { + if (schemaValidation === true) { + return hasSchema ? { schemaErrors } : false + } + if (schemaValidation === false) { + return hasBusiness ? errorObj : false + } + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } + } + + if (schemaValidation === true) { + return hasSchema ? { schemaErrors } : false + } else if (schemaValidation === false) { + return hasBusiness ? errorObj : false + } else { + return { schemaErrors, businessErrors: errorObj } + } } const replaceTimestamp = (key: any, value: any): number | undefined => { diff --git a/utils/Retail_.1.2.5/SearchInc/searchIncremental.ts b/utils/Retail_.1.2.5/SearchInc/searchIncremental.ts index 78aebb59..957074d6 100644 --- a/utils/Retail_.1.2.5/SearchInc/searchIncremental.ts +++ b/utils/Retail_.1.2.5/SearchInc/searchIncremental.ts @@ -11,9 +11,10 @@ import { } from '../..' import _ from 'lodash' -export const checkSearchIncremental = (data: any, msgIdSet: any) => { +export const checkSearchIncremental = (data: any, msgIdSet: any, schemaValidation?: boolean, stateless?: boolean) => { try { const errorObj: any = {} + const schemaErrors: any = {} if (!data || isObjectEmpty(data)) { errorObj[ApiSequence.INC_SEARCH] = 'JSON cannot be empty' return @@ -23,28 +24,38 @@ export const checkSearchIncremental = (data: any, msgIdSet: any) => { if (!message || !context || !message.intent || isObjectEmpty(message) || isObjectEmpty(message.intent)) { return { missingFields: '/context, /message, /intent or /message/intent is missing or empty' } } - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1] || 'RET11', constants.INC_SEARCH, data) + + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1] || 'RET11', constants.INC_SEARCH, data) + : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + const contextRes: any = checkContext(context, constants.SEARCH) setValue(`${ApiSequence.INC_SEARCH}_context`, context) - try { - logger.info(`Adding Message Id /${constants.INC_SEARCH}`) - if (msgIdSet.has(context.message_id)) { - errorObj[`${ApiSequence.INC_SEARCH}_msgId`] = `Message id should not be same with previous calls` + if (!stateless) { + try { + logger.info(`Adding Message Id /${constants.INC_SEARCH}`) + if (msgIdSet.has(context.message_id)) { + errorObj[`${ApiSequence.INC_SEARCH}_msgId`] = `Message id should not be same with previous calls` + } + msgIdSet.add(context.message_id) + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.INC_SEARCH}, ${error.stack}`) } - msgIdSet.add(context.message_id) - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.INC_SEARCH}, ${error.stack}`) } - - if (schemaValidation !== 'error') { + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { Object.assign(errorObj, schemaValidation) } if (!contextRes?.valid) { Object.assign(errorObj, contextRes.ERRORS) } - if (_.isEqual(data.context, getValue(`domain`))) { + if (!stateless && _.isEqual(data.context, getValue(`domain`))) { errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` } @@ -52,17 +63,20 @@ export const checkSearchIncremental = (data: any, msgIdSet: any) => { errorObj.contextCityError = 'context/city should be "*" while sending search_inc_catalog request' } - const onSearchContext: any = getValue(`${ApiSequence.ON_SEARCH}_context`) - try { - logger.info(`Comparing timestamp of /${constants.ON_SEARCH} and /${constants.INC_SEARCH}}`) - const tmpstmp = onSearchContext?.timestamp - if (_.gte(tmpstmp, context.timestamp)) { - errorObj.tmpstmp = `Timestamp for /${constants.ON_SEARCH} api cannot be greater than or equal to /${constants.INC_SEARCH} api` + if (!stateless) { + + const onSearchContext: any = getValue(`${ApiSequence.ON_SEARCH}_context`) + try { + logger.info(`Comparing timestamp of /${constants.ON_SEARCH} and /${constants.INC_SEARCH}}`) + const tmpstmp = onSearchContext?.timestamp + if (_.gte(tmpstmp, context.timestamp)) { + errorObj.tmpstmp = `Timestamp for /${constants.ON_SEARCH} api cannot be greater than or equal to /${constants.INC_SEARCH} api` + } + } catch (error: any) { + logger.info( + `Error while comparing timestamp for /${constants.ON_SEARCH} and /${constants.INC_SEARCH}} api, ${error.stack}`, + ) } - } catch (error: any) { - logger.info( - `Error while comparing timestamp for /${constants.ON_SEARCH} and /${constants.INC_SEARCH}} api, ${error.stack}`, - ) } const buyerFF = parseFloat(message.intent?.payment?.['@ondc/org/buyer_app_finder_fee_amount']) @@ -106,7 +120,27 @@ export const checkSearchIncremental = (data: any, msgIdSet: any) => { errorObj.intent = '/message/intent should have a required property tags' } - return Object.keys(errorObj).length > 0 && errorObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + + if (stateless) { + if (schemaValidation === true) { + return hasSchema ? { schemaErrors } : false + } + if (schemaValidation === false) { + return hasBusiness ? { errorObj } : false + } + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } + } + + if (schemaValidation === true) { + return hasSchema ? { schemaErrors } : false + } else if (schemaValidation === false) { + return hasBusiness ? { errorObj } : false + } else { + return { schemaErrors, businessErrors: errorObj } + } } catch (error: any) { logger.error(error.message) return { error: error.message } diff --git a/utils/Retail_.1.2.5/Select/onSelect.ts b/utils/Retail_.1.2.5/Select/onSelect.ts index a5deb0e3..3a979c61 100644 --- a/utils/Retail_.1.2.5/Select/onSelect.ts +++ b/utils/Retail_.1.2.5/Select/onSelect.ts @@ -21,7 +21,7 @@ const retailPymntTtl: { [key: string]: string } = { 'convenience fee': 'misc', offer: 'offer', } -export const checkOnSelect = (data: any, flow?: string) => { +export const checkOnSelect = (data: any, flow?: string, schemaValidation?: boolean, stateless?: boolean) => { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_SELECT]: 'JSON cannot be empty' } } @@ -30,48 +30,64 @@ export const checkOnSelect = (data: any, flow?: string) => { if (!message || !context || !message.order || isObjectEmpty(message) || isObjectEmpty(message.order)) { return { missingFields: '/context, /message, /order or /message/order is missing or empty' } } - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_SELECT, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_SELECT, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_SELECT) const errorObj: any = {} + const schemaErrors: any = {} + const checkBap = checkBppIdOrBapId(context.bap_id) const checkBpp = checkBppIdOrBapId(context.bpp_id) - try { - logger.info(`Comparing Message Ids of /${constants.SELECT} and /${constants.ON_SELECT}`) - if (!_.isEqual(getValue(`${ApiSequence.SELECT}_msgId`), context.message_id)) { - errorObj[`${ApiSequence.ON_SELECT}_msgId`] = - `Message Ids for /${constants.SELECT} and /${constants.ON_SELECT} api should be same` + if (!stateless) { + try { + logger.info(`Comparing Message Ids of /${constants.SELECT} and /${constants.ON_SELECT}`) + if (!_.isEqual(getValue(`${ApiSequence.SELECT}_msgId`), context.message_id)) { + errorObj[`${ApiSequence.ON_SELECT}_msgId`] = + `Message Ids for /${constants.SELECT} and /${constants.ON_SELECT} api should be same` + } + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.ON_SELECT}, ${error.stack}`) } - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_SELECT}, ${error.stack}`) - } - try { - logger.info(`Comparing Message Ids of /${constants.SELECT} and /${constants.ON_SELECT}`) - if (!_.isEqual(getValue(`${ApiSequence.SELECT}_msgId`), context.message_id)) { - errorObj[`${ApiSequence.ON_SELECT}_msgId`] = - `Message Ids for /${constants.SELECT} and /${constants.ON_SELECT} api should be same` + try { + logger.info(`Comparing Message Ids of /${constants.SELECT} and /${constants.ON_SELECT}`) + if (!_.isEqual(getValue(`${ApiSequence.SELECT}_msgId`), context.message_id)) { + errorObj[`${ApiSequence.ON_SELECT}_msgId`] = + `Message Ids for /${constants.SELECT} and /${constants.ON_SELECT} api should be same` + } + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.ON_SELECT}, ${error.stack}`) } - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_SELECT}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } if (checkBap) Object.assign(errorObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(errorObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(errorObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(errorObj, contextRes.ERRORS) } + // In stateless mode, stop after schema/context/basic validations + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } + } + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) const searchMessage: any = getValue(`${ApiSequence.ON_SEARCH}_message`) @@ -1715,5 +1731,8 @@ export const checkOnSelect = (data: any, flow?: string) => { } } - return Object.keys(errorObj).length > 0 && errorObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } } diff --git a/utils/Retail_.1.2.5/Select/select.ts b/utils/Retail_.1.2.5/Select/select.ts index 64476f56..d2d1b33a 100644 --- a/utils/Retail_.1.2.5/Select/select.ts +++ b/utils/Retail_.1.2.5/Select/select.ts @@ -25,7 +25,7 @@ const tagFinder = (item: { tags: any[] }, value: string): any => { return res } -export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { +export const checkSelect = (data: any, msgIdSet: any, apiSeq: any, schemaValidation?: boolean, stateless?: boolean) => { if (!data || isObjectEmpty(data)) { return { [ApiSequence.SELECT]: 'JSON cannot be empty' } } @@ -34,14 +34,15 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { if (!message || !context || !message.order || isObjectEmpty(message) || isObjectEmpty(message.order)) { return { missingFields: '/context, /message, /order or /message/order is missing or empty' } } - - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.SELECT, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.SELECT, data) + : 'skip' const contextRes: any = checkContext(context, constants.SELECT) - const onSearch: any = getValue(`${ApiSequence.ON_SEARCH}`) - const flow = getValue('flow') - + const onSearch: any = stateless ? undefined : getValue(`${ApiSequence.ON_SEARCH}`) + const flow = stateless ? undefined : getValue('flow') const errorObj: any = {} + const schemaErrors: any = {} try { if (flow === '3' && apiSeq == ApiSequence.SELECT_OUT_OF_STOCK) { @@ -50,14 +51,14 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { errorObj[`${ApiSequence.SELECT_OUT_OF_STOCK}_msgId`] = `Message id should not be same with previous calls` } msgIdSet.add(context.message_id) - setValue(`${ApiSequence.SELECT_OUT_OF_STOCK}_msgId`, data.context.message_id) + if (!stateless) setValue(`${ApiSequence.SELECT_OUT_OF_STOCK}_msgId`, data.context.message_id) } else { logger.info(`Adding Message Id /${constants.SELECT}`) if (msgIdSet.has(context.message_id)) { errorObj[`${ApiSequence.SELECT}_msgId`] = `Message id should not be same with previous calls` } msgIdSet.add(context.message_id) - setValue(`${ApiSequence.SELECT}_msgId`, data.context.message_id) + if (!stateless) setValue(`${ApiSequence.SELECT}_msgId`, data.context.message_id) } } catch (error: any) { if (flow === '3' && apiSeq == ApiSequence.SELECT_OUT_OF_STOCK) { @@ -72,8 +73,10 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { const itemsCtgrs: any = {} const itemsTat: any[] = [] - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + errorObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } const checkBap = checkBppIdOrBapId(context.bap_id) @@ -82,14 +85,22 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { if (checkBap) Object.assign(errorObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(errorObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(errorObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(errorObj, contextRes.ERRORS) } + // In stateless mode, stop after schema/context/basic validations + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } + } + const select = message.order setValue(`${ApiSequence.SELECT}`, data) @@ -217,12 +228,17 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { providerOnSelect = provider[0] setValue('providerGps', providerOnSelect?.locations[0]?.gps) - setValue('providerName', providerOnSelect?.descriptor?.name) + setValue('bap_terms', onSearch?.message?.catalog?.bpp?.descriptor?.tags?.find((t: any) => t.code === 'bap_terms')) + + // Getting Select Items + const searchItemMapping = getValue('itemMap') + const searchCustomMapping = getValue('itemMapper') + + if (searchItemMapping) Object.assign(itemMap, searchItemMapping) + if (searchCustomMapping) Object.assign(itemMapper, searchCustomMapping) } } catch (error: any) { - logger.error( - `Error while checking for valid provider in /${constants.ON_SEARCH} and /${constants.SELECT}, ${error.stack}`, - ) + logger.error(`!!Error while checking Select Item Ids in /${constants.SELECT}, ${error.stack}`) } try { @@ -689,5 +705,9 @@ export const checkSelect = (data: any, msgIdSet: any, apiSeq: any) => { } - return Object.keys(errorObj).length > 0 && errorObj + // return at the end of function + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(errorObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: errorObj } } diff --git a/utils/Retail_.1.2.5/Status/onStatusAgentAssigned.ts b/utils/Retail_.1.2.5/Status/onStatusAgentAssigned.ts index cfe12628..d3e4551d 100644 --- a/utils/Retail_.1.2.5/Status/onStatusAgentAssigned.ts +++ b/utils/Retail_.1.2.5/Status/onStatusAgentAssigned.ts @@ -2,12 +2,7 @@ import _ from 'lodash' import constants, { ApiSequence } from '../../../constants' import { logger } from '../../../shared/logger' -import { - validateSchemaRetailV2, - isObjectEmpty, - checkContext, - areTimestampsLessThanOrEqualTo, -} from '../..' +import { validateSchemaRetailV2, isObjectEmpty, checkContext, areTimestampsLessThanOrEqualTo } from '../..' import { getValue, setValue } from '../../../shared/dao' import { isStateForbiddenForRouting } from '../common/routingValidator' import { @@ -15,39 +10,63 @@ import { validateProviderCredentials, validateFulfillmentConsistency, validateRoutingTagStructure, - validateOrderUpdatedAt + validateOrderUpdatedAt, } from '../common/statusValidationHelpers' -export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusAgentAssigned = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'Agent-assigned' - try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_STATUS_AGENT_ASSIGNED]: 'JSON cannot be empty' } } - const { message, context }: any = data if (!message || !context || isObjectEmpty(message)) { return { missingFields: '/context, /message, is missing or empty' } } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' + const contextRes: any = checkContext(context, constants.ON_STATUS) + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { - onStatusObj[`${ApiSequence.ON_STATUS_AGENT_ASSIGNED}_msgId`] = `Message id should not be same with previous calls` + onStatusObj[`${ApiSequence.ON_STATUS_AGENT_ASSIGNED}_msgId`] = + `Message id should not be same with previous calls` } msgIdSet.add(context.message_id) } catch (error: any) { @@ -67,7 +86,9 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: Object.assign(onStatusObj, res.ERRORS) } } catch (error: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} context, ${error.stack}`) + logger.error( + `!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} context, ${error.stack}`, + ) } try { @@ -76,7 +97,9 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: onStatusObj.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}` } } catch (error: any) { - logger.error(`!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -106,14 +129,18 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: } try { - logger.info(`Comparing timestamp of previous /${constants.ON_STATUS} and /${constants.ON_STATUS}_${EXPECTED_STATE} API`) + logger.info( + `Comparing timestamp of previous /${constants.ON_STATUS} and /${constants.ON_STATUS}_${EXPECTED_STATE} API`, + ) if (_.gte(getValue('tmpstmp'), context.timestamp)) { onStatusObj.inVldTmstmp = `Timestamp in previous /${constants.ON_STATUS} api cannot be greater than or equal to /${constants.ON_STATUS}_${EXPECTED_STATE} api` } setValue('tmpstmp', context.timestamp) } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -122,7 +149,9 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: onStatusObj.tmpstmp1 = `Timestamp for /${constants.ON_CONFIRM} api cannot be greater than or equal to /${constants.ON_STATUS}_${EXPECTED_STATE} api` } } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -131,7 +160,9 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: onStatusObj.orderState = `Order state should be 'In-progress' for /${constants.ON_STATUS}_${EXPECTED_STATE}` } } catch (error: any) { - logger.error(`!!Error occurred while checking order state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while checking order state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate routing type - Agent-assigned is valid for both P2P and P2H2P @@ -148,15 +179,16 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: on_status.updated_at, contextTime, previousUpdatedAt, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, updatedAtErrors) - if (on_status.updated_at) { setValue('PreviousUpdatedTimestamp', on_status.updated_at) } } catch (error: any) { - logger.error(`!!Error while checking order.updated_at in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking order.updated_at in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate payment status for COD flows @@ -165,7 +197,7 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: const paymentErrors = validatePaymentStatus( on_status.payment, flow, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, paymentErrors) } catch (error: any) { @@ -179,11 +211,13 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: const providerErrors = validateProviderCredentials( on_status.provider, flow, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, providerErrors) } catch (error: any) { - logger.error(`!!Error while validating provider credentials in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating provider credentials in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } } @@ -214,9 +248,11 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: logger.info(`Checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}`) const ffDesc = ff.state?.descriptor if (!ffDesc || !ffDesc.code) { - onStatusObj[`fulfillmentState[${ff.id}]`] = `Fulfillment state descriptor is missing for /${constants.ON_STATUS}_${EXPECTED_STATE}` + onStatusObj[`fulfillmentState[${ff.id}]`] = + `Fulfillment state descriptor is missing for /${constants.ON_STATUS}_${EXPECTED_STATE}` } else if (ffDesc.code !== EXPECTED_STATE) { - onStatusObj[`fulfillmentState[${ff.id}]`] = `Fulfillment state should be '${EXPECTED_STATE}' for /${constants.ON_STATUS}_${EXPECTED_STATE} but found '${ffDesc.code}'` + onStatusObj[`fulfillmentState[${ff.id}]`] = + `Fulfillment state should be '${EXPECTED_STATE}' for /${constants.ON_STATUS}_${EXPECTED_STATE} but found '${ffDesc.code}'` } // Validate state is allowed for routing type @@ -224,11 +260,14 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: if (routingType) { const validationError = isStateForbiddenForRouting(ffDesc.code, routingType) if (validationError) { - onStatusObj[`fulfillmentStateRouting[${ff.id}]`] = `Fulfillment state '${ffDesc.code}' is not allowed for ${routingType} routing` + onStatusObj[`fulfillmentStateRouting[${ff.id}]`] = + `Fulfillment state '${ffDesc.code}' is not allowed for ${routingType} routing` } } } catch (error: any) { - logger.error(`!!Error while checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate routing tag structure @@ -236,11 +275,13 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: const routingTagErrors = validateRoutingTagStructure( ff, routingType, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, routingTagErrors) } catch (error: any) { - logger.error(`!!Error while validating routing tags for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating routing tags for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate fulfillment consistency @@ -250,16 +291,20 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: const consistencyErrors = validateFulfillmentConsistency( ff, storedFulfillment, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, consistencyErrors) } catch (error: any) { - logger.error(`!!Error while validating fulfillment consistency for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating fulfillment consistency for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } } }) } catch (error: any) { - logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${EXPECTED_STATE}`) + logger.info( + `Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${EXPECTED_STATE}`, + ) } try { @@ -269,20 +314,25 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: const currentFulfillment = on_status.fulfillments.find((f: any) => f.id === DeliveryFulfillment?.id) if (currentFulfillment) { - const pendingTimestamp = currentFulfillment.state?.descriptor?.code === 'Pending' - ? pending_timestamp - : DeliveryFulfillment?.['@ondc/org/state_updated_at'] - + const pendingTimestamp = + currentFulfillment.state?.descriptor?.code === 'Pending' + ? pending_timestamp + : DeliveryFulfillment?.['@ondc/org/state_updated_at'] + const currentTimestamp = currentFulfillment['@ondc/org/state_updated_at'] - + if (!currentTimestamp) { - onStatusObj[`fulfillmentTimestamp[${currentFulfillment.id}]`] = `'@ondc/org/state_updated_at' is required for fulfillment state updates` + onStatusObj[`fulfillmentTimestamp[${currentFulfillment.id}]`] = + `'@ondc/org/state_updated_at' is required for fulfillment state updates` } else if (pendingTimestamp && !areTimestampsLessThanOrEqualTo(pendingTimestamp, currentTimestamp)) { - onStatusObj[`fulfillmentTimestampOrder[${currentFulfillment.id}]`] = `'@ondc/org/state_updated_at' should be greater than or equal to the previous state timestamp` + onStatusObj[`fulfillmentTimestampOrder[${currentFulfillment.id}]`] = + `'@ondc/org/state_updated_at' should be greater than or equal to the previous state timestamp` } } } catch (error: any) { - logger.error(`!!Error while checking fulfillment timestamps for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking fulfillment timestamps for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -299,9 +349,14 @@ export const checkOnStatusAgentAssigned = (data: any, _state: string, msgIdSet: logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - return onStatusObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) - return { error: err.message } + logger.error(`!!Error in validation of /${constants.ON_STATUS}_${EXPECTED_STATE}, ${err.stack}`) + return { + [ApiSequence.ON_STATUS_AGENT_ASSIGNED]: `Internal error while validating /${constants.ON_STATUS}_${EXPECTED_STATE}`, + } } -} \ No newline at end of file +} diff --git a/utils/Retail_.1.2.5/Status/onStatusAtDelivery.ts b/utils/Retail_.1.2.5/Status/onStatusAtDelivery.ts index a262368c..694e0168 100644 --- a/utils/Retail_.1.2.5/Status/onStatusAtDelivery.ts +++ b/utils/Retail_.1.2.5/Status/onStatusAtDelivery.ts @@ -18,8 +18,17 @@ import { validateOrderUpdatedAt } from '../common/statusValidationHelpers' -export const checkOnStatusAtDelivery = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusAtDelivery = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'At-delivery' try { @@ -41,17 +50,27 @@ export const checkOnStatusAtDelivery = (data: any, _state: string, msgIdSet: any } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -62,8 +81,10 @@ export const checkOnStatusAtDelivery = (data: any, _state: string, msgIdSet: any logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_AT_DELIVERY}`, data) @@ -296,6 +317,18 @@ export const checkOnStatusAtDelivery = (data: any, _state: string, msgIdSet: any logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusAtDestinationHub.ts b/utils/Retail_.1.2.5/Status/onStatusAtDestinationHub.ts index 54a0b79e..53cfccfb 100644 --- a/utils/Retail_.1.2.5/Status/onStatusAtDestinationHub.ts +++ b/utils/Retail_.1.2.5/Status/onStatusAtDestinationHub.ts @@ -20,8 +20,17 @@ import { } from '../common/statusValidationHelpers' import { STATE_TRANSITIONS } from '../../../constants/fulfillmentStates' -export const checkOnStatusAtDestinationHub = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusAtDestinationHub = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'At-destination-hub' try { @@ -43,17 +52,27 @@ export const checkOnStatusAtDestinationHub = (data: any, _state: string, msgIdSe } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -64,8 +83,10 @@ export const checkOnStatusAtDestinationHub = (data: any, _state: string, msgIdSe logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_AT_DESTINATION_HUB}`, data) @@ -318,6 +339,18 @@ export const checkOnStatusAtDestinationHub = (data: any, _state: string, msgIdSe logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusAtPickup.ts b/utils/Retail_.1.2.5/Status/onStatusAtPickup.ts index a1f71cce..54f1f466 100644 --- a/utils/Retail_.1.2.5/Status/onStatusAtPickup.ts +++ b/utils/Retail_.1.2.5/Status/onStatusAtPickup.ts @@ -2,12 +2,7 @@ import _ from 'lodash' import constants, { ApiSequence } from '../../../constants' import { logger } from '../../../shared/logger' -import { - validateSchemaRetailV2, - isObjectEmpty, - checkContext, - areTimestampsLessThanOrEqualTo, -} from '../..' +import { validateSchemaRetailV2, isObjectEmpty, checkContext, areTimestampsLessThanOrEqualTo } from '../..' import { getValue, setValue } from '../../../shared/dao' import { isStateForbiddenForRouting } from '../common/routingValidator' import { @@ -15,18 +10,27 @@ import { validateProviderCredentials, validateFulfillmentConsistency, validateRoutingTagStructure, - validateOrderUpdatedAt + validateOrderUpdatedAt, } from '../common/statusValidationHelpers' -export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusAtPickup = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} const EXPECTED_STATE = 'At-pickup' - + const schemaErrors: any = {} + try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_STATUS_AT_PICKUP]: 'JSON cannot be empty' } } - + const { message, context }: any = data if (!message || !context || isObjectEmpty(message)) { return { missingFields: '/context, /message, is missing or empty' } @@ -35,23 +39,40 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, // Check if routing type is P2P const routingType = getValue('routingType') if (routingType && routingType !== 'P2P') { - return { - routingError: `/${constants.ON_STATUS}_${EXPECTED_STATE} is only valid for P2P routing, but current routing is ${routingType}` + return { + routingError: `/${constants.ON_STATUS}_${EXPECTED_STATE} is only valid for P2P routing, but current routing is ${routingType}`, } } - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) - const contextRes: any = checkContext(context, constants.ON_STATUS) + // Run schema validation conditionally + let schemaValidationResult = 'skip' + if (schemaValidation !== false) { + schemaValidationResult = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + } - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } + const contextRes: any = checkContext(context, constants.ON_STATUS) + if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -62,10 +83,6 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } - setValue(`${ApiSequence.ON_STATUS_AT_PICKUP}`, data) try { @@ -75,7 +92,9 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, Object.assign(onStatusObj, res.ERRORS) } } catch (error: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} context, ${error.stack}`) + logger.error( + `!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} context, ${error.stack}`, + ) } try { @@ -84,7 +103,9 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, onStatusObj.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}` } } catch (error: any) { - logger.error(`!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -114,14 +135,18 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, } try { - logger.info(`Comparing timestamp of previous /${constants.ON_STATUS} and /${constants.ON_STATUS}_${EXPECTED_STATE} API`) + logger.info( + `Comparing timestamp of previous /${constants.ON_STATUS} and /${constants.ON_STATUS}_${EXPECTED_STATE} API`, + ) if (_.gte(getValue('tmpstmp'), context.timestamp)) { onStatusObj.inVldTmstmp = `Timestamp in previous /${constants.ON_STATUS} api cannot be greater than or equal to /${constants.ON_STATUS}_${EXPECTED_STATE} api` } setValue('tmpstmp', context.timestamp) } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -130,7 +155,9 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, onStatusObj.tmpstmp1 = `Timestamp for /${constants.ON_CONFIRM} api cannot be greater than or equal to /${constants.ON_STATUS}_${EXPECTED_STATE} api` } } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -139,7 +166,9 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, onStatusObj.orderState = `Order state should be 'In-progress' for /${constants.ON_STATUS}_${EXPECTED_STATE}` } } catch (error: any) { - logger.error(`!!Error occurred while checking order state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error occurred while checking order state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate order.updated_at timestamp @@ -150,15 +179,17 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, on_status.updated_at, contextTime, previousUpdatedAt, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, updatedAtErrors) - + if (on_status.updated_at) { setValue('PreviousUpdatedTimestamp', on_status.updated_at) } } catch (error: any) { - logger.error(`!!Error while checking order.updated_at in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking order.updated_at in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate payment status for COD flows @@ -167,7 +198,7 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, const paymentErrors = validatePaymentStatus( on_status.payment, flow, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, paymentErrors) } catch (error: any) { @@ -181,11 +212,13 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, const providerErrors = validateProviderCredentials( on_status.provider, flow, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, providerErrors) } catch (error: any) { - logger.error(`!!Error while validating provider credentials in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating provider credentials in /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } } @@ -216,20 +249,25 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, logger.info(`Checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}`) const ffDesc = ff.state?.descriptor if (!ffDesc || !ffDesc.code) { - onStatusObj[`fulfillmentState[${ff.id}]`] = `Fulfillment state descriptor is missing for /${constants.ON_STATUS}_${EXPECTED_STATE}` + onStatusObj[`fulfillmentState[${ff.id}]`] = + `Fulfillment state descriptor is missing for /${constants.ON_STATUS}_${EXPECTED_STATE}` } else if (ffDesc.code !== EXPECTED_STATE) { - onStatusObj[`fulfillmentState[${ff.id}]`] = `Fulfillment state should be '${EXPECTED_STATE}' for /${constants.ON_STATUS}_${EXPECTED_STATE} but found '${ffDesc.code}'` + onStatusObj[`fulfillmentState[${ff.id}]`] = + `Fulfillment state should be '${EXPECTED_STATE}' for /${constants.ON_STATUS}_${EXPECTED_STATE} but found '${ffDesc.code}'` } // Validate state is allowed for routing type (should always be P2P here) if (routingType && routingType === 'P2P') { const validationError = isStateForbiddenForRouting(ffDesc.code, routingType) if (validationError) { - onStatusObj[`fulfillmentStateRouting[${ff.id}]`] = `Fulfillment state '${ffDesc.code}' is not allowed for ${routingType} routing` + onStatusObj[`fulfillmentStateRouting[${ff.id}]`] = + `Fulfillment state '${ffDesc.code}' is not allowed for ${routingType} routing` } } } catch (error: any) { - logger.error(`!!Error while checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking fulfillment state for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate routing tag structure @@ -237,11 +275,13 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, const routingTagErrors = validateRoutingTagStructure( ff, routingType || 'P2P', - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, routingTagErrors) } catch (error: any) { - logger.error(`!!Error while validating routing tags for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating routing tags for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } // Validate fulfillment consistency @@ -251,16 +291,20 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, const consistencyErrors = validateFulfillmentConsistency( ff, storedFulfillment, - `/${constants.ON_STATUS}_${EXPECTED_STATE}` + `/${constants.ON_STATUS}_${EXPECTED_STATE}`, ) Object.assign(onStatusObj, consistencyErrors) } catch (error: any) { - logger.error(`!!Error while validating fulfillment consistency for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while validating fulfillment consistency for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } } }) } catch (error: any) { - logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${EXPECTED_STATE}`) + logger.info( + `Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${EXPECTED_STATE}`, + ) } try { @@ -271,15 +315,19 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, if (currentFulfillment) { const prevTimestamp = DeliveryFulfillment?.['@ondc/org/state_updated_at'] const currentTimestamp = currentFulfillment['@ondc/org/state_updated_at'] - + if (!currentTimestamp) { - onStatusObj[`fulfillmentTimestamp[${currentFulfillment.id}]`] = `'@ondc/org/state_updated_at' is required for fulfillment state updates` + onStatusObj[`fulfillmentTimestamp[${currentFulfillment.id}]`] = + `'@ondc/org/state_updated_at' is required for fulfillment state updates` } else if (prevTimestamp && !areTimestampsLessThanOrEqualTo(prevTimestamp, currentTimestamp)) { - onStatusObj[`fulfillmentTimestampOrder[${currentFulfillment.id}]`] = `'@ondc/org/state_updated_at' should be greater than or equal to the previous state timestamp` + onStatusObj[`fulfillmentTimestampOrder[${currentFulfillment.id}]`] = + `'@ondc/org/state_updated_at' should be greater than or equal to the previous state timestamp` } } } catch (error: any) { - logger.error(`!!Error while checking fulfillment timestamps for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) + logger.error( + `!!Error while checking fulfillment timestamps for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`, + ) } try { @@ -296,9 +344,25 @@ export const checkOnStatusAtPickup = (data: any, _state: string, msgIdSet: any, logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - return onStatusObj + if (schemaValidation === true) { + // Return only schema errors (or empty if none) + if (Object.keys(schemaErrors).length === 0) return false + return schemaErrors + } else if (schemaValidation === false) { + // Return only business errors (or empty if none) + if (Object.keys(onStatusObj).length === 0) return false + return onStatusObj + } else { + // Return both combined by default + const combinedErrors = { ...schemaErrors, ...onStatusObj } + if (Object.keys(combinedErrors).length === 0) return false + return combinedErrors + } } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) + logger.error( + `!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, + JSON.stringify(err.stack), + ) return { error: err.message } } -} \ No newline at end of file +} diff --git a/utils/Retail_.1.2.5/Status/onStatusDelivered.ts b/utils/Retail_.1.2.5/Status/onStatusDelivered.ts index ebe4b4b8..ecf231a6 100644 --- a/utils/Retail_.1.2.5/Status/onStatusDelivered.ts +++ b/utils/Retail_.1.2.5/Status/onStatusDelivered.ts @@ -17,8 +17,9 @@ import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' import { validateStateForRouting } from '../common/routingValidator' -export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any) => { +export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any, schemaValidation?: boolean, stateless?: boolean) => { const onStatusObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_STATUS_DELIVERED]: 'JSON cannot be empty' } @@ -30,13 +31,30 @@ export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, return { missingFields: '/context, /message, is missing or empty' } } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' + const contextRes: any = checkContext(context, constants.ON_STATUS) + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } } + try { logger.info(`Adding Message Id /${constants.ON_STATUS_DELIVERED}`) if (msgIdSet.has(context.message_id)) { @@ -289,13 +307,13 @@ export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, if (ffState === constants.ORDER_DELIVERED) { orderDelivered = true - + // Validate state is allowed for routing type const routingType = getValue('routingType') if (routingType && !validateStateForRouting(ffState, routingType)) { onStatusObj[`fulfillmentStateRouting[${fulfillment.id}]`] = `Fulfillment state '${ffState}' is not allowed for ${routingType} routing` } - + const pickUpTime = fulfillment.start.time.timestamp const deliveryTime = fulfillment.end.time.timestamp deliveryTimestamps[fulfillment.id] = deliveryTime @@ -506,28 +524,28 @@ export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, } - try { - const credsWithProviderId = getValue('credsWithProviderId') - const providerId = on_status?.provider?.id - const confirmCreds = on_status?.provider?.creds - const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) - const expectedCreds = found?.creds - if (!expectedCreds) { - onStatusObj['MissingCreds'] = `creds must be present in /${constants.ON_STATUS_DELIVERED}` - } - if (flow === FLOW.FLOW017) { - - if (!expectedCreds) { - onStatusObj['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` - } else if (!deepCompare(expectedCreds, confirmCreds)) { - console.log('here inside else') - onStatusObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` - } - } - - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) - } + try { + const credsWithProviderId = getValue('credsWithProviderId') + const providerId = on_status?.provider?.id + const confirmCreds = on_status?.provider?.creds + const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) + const expectedCreds = found?.creds + if (!expectedCreds) { + onStatusObj['MissingCreds'] = `creds must be present in /${constants.ON_STATUS_DELIVERED}` + } + if (flow === FLOW.FLOW017) { + + if (!expectedCreds) { + onStatusObj['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` + } else if (!deepCompare(expectedCreds, confirmCreds)) { + console.log('here inside else') + onStatusObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` + } + } + + } catch (err: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + } if (flow === FLOW.FLOW01C) { const fulfillments = on_status.fulfillments const deliveryFulfillment = fulfillments.find((f: any) => f.type === 'Delivery') @@ -546,8 +564,12 @@ export const checkOnStatusDelivered = (data: any, state: string, msgIdSet: any, } } - return onStatusObj - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } catch (error: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, error.stack) + return { [ApiSequence.ON_STATUS_DELIVERED]: `Some error occurred while checking /${constants.ON_STATUS} API` } } } diff --git a/utils/Retail_.1.2.5/Status/onStatusDeliveryFailed.ts b/utils/Retail_.1.2.5/Status/onStatusDeliveryFailed.ts index 78732924..6d43929b 100644 --- a/utils/Retail_.1.2.5/Status/onStatusDeliveryFailed.ts +++ b/utils/Retail_.1.2.5/Status/onStatusDeliveryFailed.ts @@ -11,8 +11,16 @@ import { import { getValue, setValue } from '../../../shared/dao' import { isStateForbiddenForRouting } from '../common/routingValidator' -export const checkOnStatusDeliveryFailed = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any) => { +export const checkOnStatusDeliveryFailed = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'Delivery-failed' try { @@ -34,17 +42,27 @@ export const checkOnStatusDeliveryFailed = (data: any, _state: string, msgIdSet: } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -55,8 +73,10 @@ export const checkOnStatusDeliveryFailed = (data: any, _state: string, msgIdSet: logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_DELIVERY_FAILED}`, data) @@ -215,6 +235,18 @@ export const checkOnStatusDeliveryFailed = (data: any, _state: string, msgIdSet: logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusInTransit.ts b/utils/Retail_.1.2.5/Status/onStatusInTransit.ts index d7a0eff6..8eec8b88 100644 --- a/utils/Retail_.1.2.5/Status/onStatusInTransit.ts +++ b/utils/Retail_.1.2.5/Status/onStatusInTransit.ts @@ -20,8 +20,17 @@ import { } from '../common/statusValidationHelpers' import { STATE_TRANSITIONS } from '../../../constants/fulfillmentStates' -export const checkOnStatusInTransit = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusInTransit = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'In-transit' try { @@ -43,17 +52,27 @@ export const checkOnStatusInTransit = (data: any, _state: string, msgIdSet: any, } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -64,8 +83,10 @@ export const checkOnStatusInTransit = (data: any, _state: string, msgIdSet: any, logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_IN_TRANSIT}`, data) @@ -318,6 +339,18 @@ export const checkOnStatusInTransit = (data: any, _state: string, msgIdSet: any, logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusOutForDelivery.ts b/utils/Retail_.1.2.5/Status/onStatusOutForDelivery.ts index d091084f..f400caa4 100644 --- a/utils/Retail_.1.2.5/Status/onStatusOutForDelivery.ts +++ b/utils/Retail_.1.2.5/Status/onStatusOutForDelivery.ts @@ -17,8 +17,10 @@ import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' import { validateStateForRouting } from '../common/routingValidator' -export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any) => { +export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any, schemaValidation?: boolean, + stateless?: boolean) => { const onStatusObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_STATUS_OUT_FOR_DELIVERY]: 'JSON cannot be empty' } @@ -31,12 +33,20 @@ export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' + const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) - } + + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + + try { logger.info(`Adding Message Id /${constants.ON_STATUS_OUT_FOR_DELIVERY}`) if (msgIdSet.has(context.message_id)) { @@ -50,10 +60,15 @@ export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } + + setValue(`${ApiSequence.ON_STATUS_OUT_FOR_DELIVERY}`, data) @@ -62,6 +77,12 @@ export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: setValue(`out_for_delivery_message_id`, out_for_delivery_message_id) + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } try { logger.info( `Comparing message_id for unsolicited calls for ${constants.ON_STATUS}.packed and ${constants.ON_STATUS}.out_for_delivery`, @@ -851,8 +872,12 @@ export const checkOnStatusOutForDelivery = (data: any, state: string, msgIdSet: } - return onStatusObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + logger.error(`Error in onStatusOutForDelivery - ${err.stack}`) + return { schemaErrors: { error: 'Internal Server Error' }, businessErrors: {} } } } diff --git a/utils/Retail_.1.2.5/Status/onStatusOutForPickup.ts b/utils/Retail_.1.2.5/Status/onStatusOutForPickup.ts index d765351e..b9abf43a 100644 --- a/utils/Retail_.1.2.5/Status/onStatusOutForPickup.ts +++ b/utils/Retail_.1.2.5/Status/onStatusOutForPickup.ts @@ -20,8 +20,17 @@ import { } from '../common/statusValidationHelpers' import { STATE_TRANSITIONS } from '../../../constants/fulfillmentStates' -export const checkOnStatusOutForPickup = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusOutForPickup = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'Out-for-pickup' try { @@ -43,17 +52,27 @@ export const checkOnStatusOutForPickup = (data: any, _state: string, msgIdSet: a } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -64,8 +83,10 @@ export const checkOnStatusOutForPickup = (data: any, _state: string, msgIdSet: a logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_OUT_FOR_PICKUP}`, data) @@ -318,6 +339,18 @@ export const checkOnStatusOutForPickup = (data: any, _state: string, msgIdSet: a logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusPacked.ts b/utils/Retail_.1.2.5/Status/onStatusPacked.ts index eccbfbf3..67b91210 100644 --- a/utils/Retail_.1.2.5/Status/onStatusPacked.ts +++ b/utils/Retail_.1.2.5/Status/onStatusPacked.ts @@ -16,8 +16,16 @@ import { getValue, setValue } from '../../../shared/dao' import { FLOW } from '../../enum' import { validateStateForRouting } from '../common/routingValidator' -export const checkOnStatusPacked = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any) => { +export const checkOnStatusPacked = ( + data: any, + state: string, + msgIdSet: any, + fulfillmentsItemsSet: any, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_STATUS_PACKED]: 'JSON cannot be empty' } @@ -31,22 +39,34 @@ export const checkOnStatusPacked = (data: any, state: string, msgIdSet: any, ful return { missingFields: '/context, /message, is missing or empty' } } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2('RET11', constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false ? validateSchemaRetailV2('RET11', constants.ON_STATUS, data) : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // If stateless is requested, return early with segregated errors + if (stateless || schemaValidation !== undefined) { + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS_PACKED}`) if (msgIdSet.has(context.message_id)) { @@ -492,8 +512,19 @@ export const checkOnStatusPacked = (data: any, state: string, msgIdSet: any, ful } } } + // Return format: preserve original behavior unless optional flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + // Legacy behavior for validate endpoint: include schema errors into main object + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) } -} +} \ No newline at end of file diff --git a/utils/Retail_.1.2.5/Status/onStatusPending.ts b/utils/Retail_.1.2.5/Status/onStatusPending.ts index ce6e5a4b..3d0ec430 100644 --- a/utils/Retail_.1.2.5/Status/onStatusPending.ts +++ b/utils/Retail_.1.2.5/Status/onStatusPending.ts @@ -4,469 +4,524 @@ import constants, { ApiSequence, PAYMENT_STATUS } from '../../../constants' import { logger } from '../../../shared/logger' import { FLOW } from '../../enum' import { - validateSchemaRetailV2, - isObjectEmpty, - checkContext, - areTimestampsLessThanOrEqualTo, - payment_status, - compareCoordinates, - compareTimeRanges, - getProviderId, - deepCompare, + validateSchemaRetailV2, + isObjectEmpty, + checkContext, + areTimestampsLessThanOrEqualTo, + payment_status, + compareCoordinates, + compareTimeRanges, + getProviderId, + deepCompare, } from '../..' import { getValue, setValue } from '../../../shared/dao' import { extractRoutingType, getDefaultRouting, isValidRoutingType } from '../common/routingValidator' -export const checkOnStatusPending = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any) => { - const onStatusObj: any = {} - const flow = getValue('flow') - const replaceValue = getValue('replaceValue)') - const { message, context }: any = data - if (!message || !context || isObjectEmpty(message)) { - return { missingFields: '/context, /message, is missing or empty' } - } - try { - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) - const contextRes: any = checkContext(context, constants.ON_STATUS) - - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) - } - - if (!contextRes?.valid) { - Object.assign(onStatusObj, contextRes.ERRORS) - } - - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` - } - - try { - logger.info(`Adding Message Id /${constants.ON_STATUS_PENDING}`) - if (msgIdSet.has(context.message_id)) { - onStatusObj[`${ApiSequence.ON_STATUS_PENDING}_msgId`] = `Message id should not be same with previous calls` - } - msgIdSet.add(context.message_id) - } catch (error: any) { - logger.error(`!!Error while checking message id for /${constants.ON_STATUS_PENDING}, ${error.stack}`) - } - - setValue(`${ApiSequence.ON_STATUS_PENDING}`, data) - setValue('pending_message_id', context.message_id) - - try { - logger.info(`Checking context for /${constants.ON_STATUS} API`) //checking context - const res: any = checkContext(context, constants.ON_STATUS) - if (!res.valid) { - Object.assign(onStatusObj, res.ERRORS) - } - } catch (error: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} context, ${error.stack}`) - } - - try { - logger.info(`Comparing city of /${constants.SEARCH} and /${constants.ON_STATUS}`) - if (!_.isEqual(searchContext.city, context.city)) { - onStatusObj.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_STATUS}` - } - } catch (error: any) { - logger.error(`!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}, ${error.stack}`) - } - - try { - logger.info(`Comparing transaction Ids of /${constants.SELECT} and /${constants.ON_STATUS}`) - if (!_.isEqual(getValue('txnId'), context.transaction_id)) { - onStatusObj.txnId = `Transaction Id should be same from /${constants.SELECT} onwards` - } - } catch (error: any) { - logger.info( - `!!Error while comparing transaction ids for /${constants.SELECT} and /${constants.ON_STATUS} api, ${error.stack}`, - ) - } - - } catch (error:any) { - console.log(`Error while checking ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING}`,error.message) - } - try { - if(state === ApiSequence.REPLACEMENT_ON_STATUS_PENDING || replaceValue === "yes"){ - const replacementFulfillment = getValue("replacementFulfillment") - console.log("replacementFulfillment",replacementFulfillment); - - if(!replacementFulfillment){ - onStatusObj["rplcmnt"] = `Fulfillment for replacement is required.` - return - } - // const onUpdateReplacementState = replacementFulfillment.state.descriptor.code - const on_status = message.order - try { - const orderState = on_status.state ? (on_status.state === "Completed" ? true : false) : false - if(!orderState){ - onStatusObj["rplcmntOrderState"] = `Mismatch ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING} and ${ApiSequence.ON_UPDATE_REPLACEMENT} and order state should be completed` - } - logger.info(`Comparing order Id in /${constants.ON_CONFIRM} and /${constants.REPLACEMENT_ON_STATUS_PENDING}`) - if (on_status.id != getValue('cnfrmOrdrId')) { - logger.info(`Order id (/${constants.REPLACEMENT_ON_STATUS_PENDING}) mismatches with /${constants.CONFIRM})`) - onStatusObj.onStatusOdrId = `Order id in /${constants.CONFIRM} and /${constants.REPLACEMENT_ON_STATUS_PENDING} do not match` - } - } catch (error) { - logger.info( - `!!Error while comparing order id in /${constants.REPLACEMENT_ON_STATUS_PENDING} and /${constants.CONFIRM}`, - error, - ) - } - try { - // Checking fulfillment.id, fulfillment.type and tracking - logger.info('Checking fulfillment.id, fulfillment.type and tracking') - on_status.fulfillments.forEach((ff: any) => { - let ffId = '' - - if (!ff.id) { - logger.info(`Fulfillment Id must be present `) - onStatusObj['ffId'] = `Fulfillment Id must be present` - } - - ffId = ff.id - if (ff.type != "Cancel") { - if (getValue(`${ffId}_tracking`)) { - if (ff.tracking === false || ff.tracking === true) { - if (getValue(`${ffId}_tracking`) != ff.tracking) { - logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) - onStatusObj['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` - } - } else { - logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) - onStatusObj['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` - } - } - } - }) - } catch (error: any) { - logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${state}`) - } - - try { - logger.info(`Storing delivery fulfillment if not present in ${constants.ON_CONFIRM} and comparing if present`) - - const deliveryFulfillment = on_status.fulfillments.find((fulfillment: any) => fulfillment.id === replacementFulfillment.id) - console.log("deliveryFulfillment in replacement",deliveryFulfillment); - - if(!deliveryFulfillment){ - onStatusObj["rplcmnt-fulfillment"] = `Fulfillment with id :${replacementFulfillment.id} not found in ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING} fulfillments` - return onStatusObj - } - try { - setValue(`replacementFulfillmentPendingStatus`, deliveryFulfillment) - if (!deliveryFulfillment['@ondc/org/TAT']) { - onStatusObj[`message.order.fulfillments[${deliveryFulfillment.id}]`] = - `'TAT' must be provided in message/order/fulfillments` - } - // Comparing on_confirm delivery fulfillment with on_update replace fulfillment - const ffDesc = deliveryFulfillment.state.descriptor - - const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false - setValue(`ffIdPrecancel`, ffDesc?.code) - if (!ffStateCheck) { - const key = `ffState:fulfillment[id]:${deliveryFulfillment.id}` - onStatusObj[key] = `default fulfillments state is missing in /${constants.ON_UPDATE_REPLACEMENT}` - } - - if (!deliveryFulfillment.start || !deliveryFulfillment.end) { - onStatusObj.ffstartend = `fulfillments start and end locations are mandatory for fulfillment id: ${deliveryFulfillment.id}` - } - - try { - if (!compareCoordinates(deliveryFulfillment.start.location.gps, getValue('providerGps'))) { - onStatusObj.sellerGpsErr = `store gps location /fulfillments/:${deliveryFulfillment.id}/start/location/gps can't change` - } - } catch (error: any) { - logger.error(`!!Error while checking store location in /${constants.ON_UPDATE_REPLACEMENT}, ${error.stack}`) - } - - try { - if (!getValue('providerName')) { - onStatusObj.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_UPDATE_REPLACEMENT}` - } else if (!_.isEqual(deliveryFulfillment.start.location.descriptor.name, getValue('providerName'))) { - onStatusObj.sellerNameErr = `store name /fulfillments/start/location/descriptor/name can't change with fulfillment id: ${deliveryFulfillment.id}` - } - } catch (error: any) { - logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) - } - - if (!_.isEqual(deliveryFulfillment.end.location.gps, getValue('buyerGps'))) { - onStatusObj.buyerGpsErr = `fulfillments.end.location gps with id ${deliveryFulfillment.id} is not matching with gps in /on_conifrm` - } - - if (!_.isEqual(deliveryFulfillment.end.location.address.area_code, getValue('buyerAddr'))) { - onStatusObj.gpsErr = `fulfillments.end.location.address.area_code with id ${deliveryFulfillment.id} is not matching with area_code in /on_confirm` - } - } catch (error: any) { - logger.error( - `Error while comparing fulfillment obj ${ApiSequence.ON_UPDATE_REPLACEMENT} and ${ApiSequence.ON_CONFIRM}`, - error.stack, - ) - } - } catch (error: any) { - logger.error(`Error while Storing delivery fulfillment, ${error.stack}`) - } - - } - if (state === "pending") { - try { - const onConfirmOrderState = getValue('onCnfrmState') - if (!data || isObjectEmpty(data)) { - if (onConfirmOrderState === "Accepted") - return - return { [ApiSequence.ON_STATUS_PENDING]: 'JSON cannot be empty' } - } - - // if (onConfirmOrderState === "Accepted") - // return { errmsg: "When the onConfirm Order State is 'Accepted', the on_status_pending is not required!" } - const on_status = message.order - try { - logger.info(`Comparing order Id in /${constants.ON_CONFIRM} and /${constants.ON_STATUS}_${state}`) - if (on_status.id != getValue('cnfrmOrdrId')) { - logger.info(`Order id (/${constants.ON_STATUS}_${state}) mismatches with /${constants.CONFIRM})`) - onStatusObj.onStatusOdrId = `Order id in /${constants.CONFIRM} and /${constants.ON_STATUS}_${state} do not match` - } - } catch (error) { - logger.info( - `!!Error while comparing order id in /${constants.ON_STATUS}_${state} and /${constants.CONFIRM}`, - error, - ) - } - - try { - // Checking fulfillment.id, fulfillment.type and tracking - logger.info('Checking fulfillment.id, fulfillment.type and tracking') - on_status.fulfillments.forEach((ff: any) => { - let ffId = '' - - if (!ff.id) { - logger.info(`Fulfillment Id must be present `) - onStatusObj['ffId'] = `Fulfillment Id must be present` - } - - ffId = ff.id - if (ff.type != "Cancel") { - if (getValue(`${ffId}_tracking`)) { - if (ff.tracking === false || ff.tracking === true) { - if (getValue(`${ffId}_tracking`) != ff.tracking) { - logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) - onStatusObj['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` - } - } else { - logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) - onStatusObj['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` - } - } - } - }) - } catch (error: any) { - logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}`) - } - - try { - logger.info(`Storing delivery fulfillment if not present in ${constants.ON_CONFIRM} and comparing if present`) - const storedFulfillment = getValue(`deliveryFulfillment`) - console.log("storedFulfillment in on_status pending",storedFulfillment,); - - const deliveryFulfillment = on_status.fulfillments.filter((fulfillment: any) => fulfillment.type === 'Delivery') - console.log("deliveryFulfillment",deliveryFulfillment); - - if (!storedFulfillment) { - setValue('deliveryFulfillment', deliveryFulfillment[0]) - setValue('deliveryFulfillmentAction', ApiSequence.ON_STATUS_PENDING) - } else { - const storedFulfillmentAction = getValue('deliveryFulfillmentAction') - const fulfillmentRangeerrors = compareTimeRanges(storedFulfillment, storedFulfillmentAction, deliveryFulfillment[0], ApiSequence.ON_STATUS_PENDING) - - if (fulfillmentRangeerrors) { - let i = 0 - const len = fulfillmentRangeerrors.length - while (i < len) { - const key = `fulfilmntRngErr${i}` - onStatusObj[key] = `${fulfillmentRangeerrors[i]}` - i++ - } - } - } - } catch (error: any) { - logger.error(`Error while processing fulfillment(s): ${error.stack}`) - } - - - - try { - logger.info(`Validating order state`) - setValue('orderState', on_status.state) - if (on_status.state !== 'Accepted') { - onStatusObj[`order_state`] = - `Order state should be accepted whenever Status is being sent 'Accepted'. Current state: ${on_status.state}` - } - } catch (error: any) { - logger.error(`Error while validating order state, ${error.stack}`) - } - - try { - if (!_.isEqual(getValue(`cnfrmTmpstmp`), on_status.created_at)) { - onStatusObj.tmpstmp = `Created At timestamp for /${constants.ON_STATUS}_${state} should be equal to context timestamp at ${constants.CONFIRM}` - } - } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${state}, ${error.stack}`) - } - - try { - logger.info(`Comparing timestamp of /${constants.ON_CONFIRM} and /${constants.ON_STATUS}_${state} API`) - if (_.gte(getValue('onCnfrmtmpstmp'), context.timestamp)) { - onStatusObj.tmpstmp1 = `Timestamp for /${constants.ON_CONFIRM} api cannot be greater than or equal to /${constants.ON_STATUS}_${state} api` - } - setValue('tmpstmp', context.timestamp) - } catch (error: any) { - logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${state}, ${error.stack}`) - } - - const contextTime = context.timestamp - try { - logger.info(`Comparing order.updated_at and context timestamp for /${constants.ON_STATUS}_${state} API`) - - if (!areTimestampsLessThanOrEqualTo(on_status.updated_at, contextTime)) { - onStatusObj.tmpstmp2 = `order.updated_at timestamp should be less than or eqaul to context timestamp for /${constants.ON_STATUS}_${state} api` - } - } catch (error: any) { - logger.error( - `!!Error occurred while comparing order updated at for /${constants.ON_STATUS}_${state}, ${error.stack}`, - ) - } - try { - logger.info(`Checking if transaction_id is present in message.order.payment`) - const payment = on_status.payment - const status = payment_status(payment, flow) - if (!status) { - onStatusObj['message/order/transaction_id'] = `Transaction_id missing in message/order/payment` - } - } catch (err: any) { - logger.error(`Error while checking transaction is in message.order.payment`) - } - try { - if (flow === FLOW.FLOW012) { - logger.info('Payment status check in on status pending call') - const payment = on_status.payment - if (payment.status !== PAYMENT_STATUS.NOT_PAID) { - logger.error(`Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`); - onStatusObj.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` - } - } - } catch (err: any) { - logger.error('Error while checking payment in message/order/payment: ' + err.message); - } - if (flow === '6' || flow === '2' || flow === '3' || flow === '5') { - try { - // For Delivery Object - const fulfillments = on_status.fulfillments - if (!fulfillments.length) { - const key = `missingFulfillments` - onStatusObj[key] = `missingFulfillments is mandatory for ${ApiSequence.ON_STATUS_PENDING}` - } - else { - const deliveryObjArr = _.filter(fulfillments, { type: "Delivery" }) - if (!deliveryObjArr.length) { - onStatusObj[`message/order.fulfillments/`] = `Delivery fullfillment must be present in ${ApiSequence.ON_STATUS_PENDING}` - } - else { - const deliverObj = deliveryObjArr[0] - delete deliverObj?.state - delete deliverObj?.tags - delete deliverObj?.start?.instructions - delete deliverObj?.end?.instructions - fulfillmentsItemsSet.add(deliverObj) - } - } - - } catch (error: any) { - logger.error(`Error while checking Fulfillments Delivery Obj in /${ApiSequence.ON_STATUS_PENDING}, ${error.stack}`) - } - } - try { - const credsWithProviderId = getValue('credsWithProviderId') - const providerId = on_status?.provider?.id - const confirmCreds = on_status?.provider?.creds - const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) - const expectedCreds = found?.creds - if (!expectedCreds) { - onStatusObj['MissingCreds'] = - `creds must be present in /${constants.ON_CANCEL} same as in /${constants.ON_SEARCH}` - } - if (flow === FLOW.FLOW017) { - if (!expectedCreds) { - onStatusObj['MissingCreds'] = `creds must be present in /${constants.ON_STATUS_PENDING} ` - } else if (!deepCompare(expectedCreds, confirmCreds)) { - onStatusObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` - } - } - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) - } - if (flow === FLOW.FLOW01C) { - const fulfillments = on_status.fulfillments - const deliveryFulfillment = fulfillments.find((f: any) => f.type === 'Delivery') - - if (!deliveryFulfillment.hasOwnProperty('provider_id')) { - onStatusObj['missingFulfillments'] = - `provider_id must be present in ${ApiSequence.ON_STATUS_PENDING} as order is accepted` - } - - const id = getProviderId(deliveryFulfillment) - const fulfillmentProviderId = getValue('fulfillmentProviderId') - - if (deliveryFulfillment.hasOwnProperty('provider_id') && id !== fulfillmentProviderId) { - onStatusObj['providerIdMismatch'] = - `provider_id in fulfillment in ${ApiSequence.ON_CONFIRM} does not match expected provider_id: expected '${fulfillmentProviderId}' in ${ApiSequence.ON_STATUS_PENDING} but got ${id}` - } - } - - // Validate routing type for retail 1.2.5 - try { - logger.info(`Checking routing type in /${constants.ON_STATUS_PENDING}`) - const onConfirmOrderState = getValue('onCnfrmState') - const storedRoutingType = getValue('routingType') - const domain = getValue('domain') - - // If order state was 'Created' in on_confirm, routing must be provided in on_status_pending - if (onConfirmOrderState === 'Created') { - const routingType = extractRoutingType(on_status.fulfillments) - - if (!routingType) { - onStatusObj.routingType = `Routing type tag is mandatory in delivery fulfillment when order.state was 'Created' in /${constants.ON_CONFIRM}` - } else if (!isValidRoutingType(routingType)) { - onStatusObj.routingType = `Invalid routing type '${routingType}'. Must be one of: P2P, P2H2P` - } else { - // If routing was already stored (from on_confirm), check for consistency - if (storedRoutingType && storedRoutingType !== routingType) { - onStatusObj.routingTypeMismatch = `Routing type mismatch: '${routingType}' in /${constants.ON_STATUS_PENDING} does not match '${storedRoutingType}' from /${constants.ON_CONFIRM}` - } else if (!storedRoutingType) { - // Store routing type for subsequent validations - setValue('routingType', routingType) - logger.info(`Stored routing type: ${routingType} for domain: ${domain}`) - } - } - } - - // If no routing type has been stored yet, use default based on domain - if (!getValue('routingType')) { - const defaultRouting = getDefaultRouting(domain) - setValue('routingType', defaultRouting) - logger.info(`No routing type found, using default: ${defaultRouting} for domain: ${domain}`) - } - } catch (error: any) { - logger.error(`Error while checking routing type in /${constants.ON_STATUS_PENDING}: ${error.stack}`) - } - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) - } - } - return onStatusObj - } catch (error:any) { - console.log(`Error while validationg flow ${flow} for replacement ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING}`,error.message) - } -} +export const checkOnStatusPending = ( + data: any, + state: string, + msgIdSet: any, + fulfillmentsItemsSet: any, + schemaValidation?: boolean, + stateless?: boolean +) => { + const businessErrors: any = {} + const schemaErrors: any = {} + + try { + // 1. Basic validations (always run) + const { message, context }: any = data + if (!message || !context || isObjectEmpty(message)) { + return { missingFields: '/context, /message, is missing or empty' } + } + + // 2. Schema validation (conditional) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + + // 3. Context validation (always run) + const contextRes: any = checkContext(context, constants.ON_STATUS) + if (!contextRes?.valid) { + Object.assign(businessErrors, contextRes.ERRORS) + } + + // 4. Message ID validation + try { + logger.info(`Adding Message Id /${constants.ON_STATUS_PENDING}`) + if (msgIdSet.has(context.message_id)) { + businessErrors[`${ApiSequence.ON_STATUS_PENDING}_msgId`] = `Message id should not be same with previous calls` + } + msgIdSet.add(context.message_id) + if (!stateless) setValue('pending_message_id', context.message_id) + } catch (error: any) { + logger.error(`!!Error while checking message id for /${constants.ON_STATUS_PENDING}, ${error.stack}`) + } + + // 5. Early return for stateless mode + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors } + } + const combinedErrors = { ...schemaErrors, ...businessErrors } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + + // 6. Stateful validations (only when stateless = false) + const flow = getValue('flow') + const replaceValue = getValue('replaceValue)') + + // Domain consistency check (requires previous state) + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + businessErrors[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + + setValue(`${ApiSequence.ON_STATUS_PENDING}`, data) + + try { + logger.info(`Checking context for /${constants.ON_STATUS} API`) //checking context + const res: any = checkContext(context, constants.ON_STATUS) + if (!res.valid) { + Object.assign(businessErrors, res.ERRORS) + } + } catch (error: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} context, ${error.stack}`) + } + + try { + logger.info(`Comparing city of /${constants.SEARCH} and /${constants.ON_STATUS}`) + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) + if (!_.isEqual(searchContext.city, context.city)) { + businessErrors.city = `City code mismatch in /${constants.SEARCH} and /${constants.ON_STATUS}` + } + } catch (error: any) { + logger.error(`!!Error while comparing city in /${constants.SEARCH} and /${constants.ON_STATUS}, ${error.stack}`) + } + + try { + logger.info(`Comparing transaction Ids of /${constants.SELECT} and /${constants.ON_STATUS}`) + if (!_.isEqual(getValue('txnId'), context.transaction_id)) { + businessErrors.txnId = `Transaction Id should be same from /${constants.SELECT} onwards` + } + } catch (error: any) { + logger.info( + `!!Error while comparing transaction ids for /${constants.SELECT} and /${constants.ON_STATUS} api, ${error.stack}`, + ) + } + + try { + if(state === ApiSequence.REPLACEMENT_ON_STATUS_PENDING || replaceValue === "yes"){ + const replacementFulfillment = getValue("replacementFulfillment") + console.log("replacementFulfillment", replacementFulfillment); + + if(!replacementFulfillment){ + businessErrors["rplcmnt"] = `Fulfillment for replacement is required.` + // Return logic for early exit + // const hasSchema = Object.keys(schemaErrors).length > 0 + // const hasBusiness = Object.keys(businessErrors).length > 0 + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors } + } + const combinedErrors = { ...schemaErrors, ...businessErrors } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + // const onUpdateReplacementState = replacementFulfillment.state.descriptor.code + const on_status = message.order + try { + const orderState = on_status.state ? (on_status.state === "Completed" ? true : false) : false + if(!orderState){ + businessErrors["rplcmntOrderState"] = `Mismatch ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING} and ${ApiSequence.ON_UPDATE_REPLACEMENT} and order state should be completed` + } + logger.info(`Comparing order Id in /${constants.ON_CONFIRM} and /${constants.REPLACEMENT_ON_STATUS_PENDING}`) + if (on_status.id != getValue('cnfrmOrdrId')) { + logger.info(`Order id (/${constants.REPLACEMENT_ON_STATUS_PENDING}) mismatches with /${constants.CONFIRM})`) + businessErrors.onStatusOdrId = `Order id in /${constants.CONFIRM} and /${constants.REPLACEMENT_ON_STATUS_PENDING} do not match` + } + } catch (error) { + logger.info( + `!!Error while comparing order id in /${constants.REPLACEMENT_ON_STATUS_PENDING} and /${constants.CONFIRM}`, + error, + ) + } + try { + // Checking fulfillment.id, fulfillment.type and tracking + logger.info('Checking fulfillment.id, fulfillment.type and tracking') + on_status.fulfillments.forEach((ff: any) => { + let ffId = '' + + if (!ff.id) { + logger.info(`Fulfillment Id must be present `) + businessErrors['ffId'] = `Fulfillment Id must be present` + } + + ffId = ff.id + if (ff.type != "Cancel") { + if (getValue(`${ffId}_tracking`)) { + if (ff.tracking === false || ff.tracking === true) { + if (getValue(`${ffId}_tracking`) != ff.tracking) { + logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) + businessErrors['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` + } + } else { + logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) + businessErrors['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` + } + } + } + }) + } catch (error: any) { + logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}_${state}`) + } + + try { + logger.info(`Storing delivery fulfillment if not present in ${constants.ON_CONFIRM} and comparing if present`) + + const deliveryFulfillment = on_status.fulfillments.find((fulfillment: any) => fulfillment.id === replacementFulfillment.id) + console.log("deliveryFulfillment in replacement", deliveryFulfillment); + + if(!deliveryFulfillment){ + businessErrors["rplcmnt-fulfillment"] = `Fulfillment with id :${replacementFulfillment.id} not found in ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING} fulfillments` + // Return logic + // const hasSchema = Object.keys(schemaErrors).length > 0 + // const hasBusiness = Object.keys(businessErrors).length > 0 + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors } + } + const combinedErrors = { ...schemaErrors, ...businessErrors } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + } + try { + setValue(`replacementFulfillmentPendingStatus`, deliveryFulfillment) + if (!deliveryFulfillment['@ondc/org/TAT']) { + businessErrors[`message.order.fulfillments[${deliveryFulfillment.id}]`] = + `'TAT' must be provided in message/order/fulfillments` + } + // Comparing on_confirm delivery fulfillment with on_update replace fulfillment + const ffDesc = deliveryFulfillment.state.descriptor + + const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false + setValue(`ffIdPrecancel`, ffDesc?.code) + if (!ffStateCheck) { + const key = `ffState:fulfillment[id]:${deliveryFulfillment.id}` + businessErrors[key] = `default fulfillments state is missing in /${constants.ON_UPDATE_REPLACEMENT}` + } + + if (!deliveryFulfillment.start || !deliveryFulfillment.end) { + businessErrors.ffstartend = `fulfillments start and end locations are mandatory for fulfillment id: ${deliveryFulfillment.id}` + } + + try { + if (!compareCoordinates(deliveryFulfillment.start.location.gps, getValue('providerGps'))) { + businessErrors.sellerGpsErr = `store gps location /fulfillments/:${deliveryFulfillment.id}/start/location/gps can't change` + } + } catch (error: any) { + logger.error(`!!Error while checking store location in /${constants.ON_UPDATE_REPLACEMENT}, ${error.stack}`) + } + + try { + if (!getValue('providerName')) { + businessErrors.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_UPDATE_REPLACEMENT}` + } else if (!_.isEqual(deliveryFulfillment.start.location.descriptor.name, getValue('providerName'))) { + businessErrors.sellerNameErr = `store name /fulfillments/start/location/descriptor/name can't change with fulfillment id: ${deliveryFulfillment.id}` + } + } catch (error: any) { + logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) + } + + if (!_.isEqual(deliveryFulfillment.end.location.gps, getValue('buyerGps'))) { + businessErrors.buyerGpsErr = `fulfillments.end.location gps with id ${deliveryFulfillment.id} is not matching with gps in /on_conifrm` + } + + if (!_.isEqual(deliveryFulfillment.end.location.address.area_code, getValue('buyerAddr'))) { + businessErrors.gpsErr = `fulfillments.end.location.address.area_code with id ${deliveryFulfillment.id} is not matching with area_code in /on_confirm` + } + } catch (error: any) { + logger.error( + `Error while comparing fulfillment obj ${ApiSequence.ON_UPDATE_REPLACEMENT} and ${ApiSequence.ON_CONFIRM}`, + error.stack, + ) + } + } catch (error: any) { + logger.error(`Error while Storing delivery fulfillment, ${error.stack}`) + } + } + + if (state === "pending") { + try { + const onConfirmOrderState = getValue('onCnfrmState') + if (!data || isObjectEmpty(data)) { + if (onConfirmOrderState === "Accepted") + return + return { [ApiSequence.ON_STATUS_PENDING]: 'JSON cannot be empty' } + } + + // if (onConfirmOrderState === "Accepted") + // return { errmsg: "When the onConfirm Order State is 'Accepted', the on_status_pending is not required!" } + const on_status = message.order + try { + logger.info(`Comparing order Id in /${constants.ON_CONFIRM} and /${constants.ON_STATUS}_${state}`) + if (on_status.id != getValue('cnfrmOrdrId')) { + logger.info(`Order id (/${constants.ON_STATUS}_${state}) mismatches with /${constants.CONFIRM})`) + businessErrors.onStatusOdrId = `Order id in /${constants.CONFIRM} and /${constants.ON_STATUS}_${state} do not match` + } + } catch (error) { + logger.info( + `!!Error while comparing order id in /${constants.ON_STATUS}_${state} and /${constants.CONFIRM}`, + error, + ) + } + + try { + // Checking fulfillment.id, fulfillment.type and tracking + logger.info('Checking fulfillment.id, fulfillment.type and tracking') + on_status.fulfillments.forEach((ff: any) => { + let ffId = '' + + if (!ff.id) { + logger.info(`Fulfillment Id must be present `) + businessErrors['ffId'] = `Fulfillment Id must be present` + } + + ffId = ff.id + if (ff.type != "Cancel") { + if (getValue(`${ffId}_tracking`)) { + if (ff.tracking === false || ff.tracking === true) { + if (getValue(`${ffId}_tracking`) != ff.tracking) { + logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) + businessErrors['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` + } + } else { + logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) + businessErrors['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` + } + } + } + }) + } catch (error: any) { + logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}`) + } + + try { + logger.info(`Storing delivery fulfillment if not present in ${constants.ON_CONFIRM} and comparing if present`) + const storedFulfillment = getValue(`deliveryFulfillment`) + console.log("storedFulfillment in on_status pending", storedFulfillment); + + const deliveryFulfillment = on_status.fulfillments.filter((fulfillment: any) => fulfillment.type === 'Delivery') + console.log("deliveryFulfillment", deliveryFulfillment); + + if (!storedFulfillment) { + setValue('deliveryFulfillment', deliveryFulfillment[0]) + setValue('deliveryFulfillmentAction', ApiSequence.ON_STATUS_PENDING) + } else { + const storedFulfillmentAction = getValue('deliveryFulfillmentAction') + const fulfillmentRangeerrors = compareTimeRanges(storedFulfillment, storedFulfillmentAction, deliveryFulfillment[0], ApiSequence.ON_STATUS_PENDING) + + if (fulfillmentRangeerrors) { + let i = 0 + const len = fulfillmentRangeerrors.length + while (i < len) { + const key = `fulfilmntRngErr${i}` + businessErrors[key] = `${fulfillmentRangeerrors[i]}` + i++ + } + } + } + } catch (error: any) { + logger.error(`Error while processing fulfillment(s): ${error.stack}`) + } + + try { + logger.info(`Validating order state`) + setValue('orderState', on_status.state) + if (on_status.state !== 'Accepted') { + businessErrors[`order_state`] = + `Order state should be accepted whenever Status is being sent 'Accepted'. Current state: ${on_status.state}` + } + } catch (error: any) { + logger.error(`Error while validating order state, ${error.stack}`) + } + + try { + if (!_.isEqual(getValue(`cnfrmTmpstmp`), on_status.created_at)) { + businessErrors.tmpstmp = `Created At timestamp for /${constants.ON_STATUS}_${state} should be equal to context timestamp at ${constants.CONFIRM}` + } + } catch (error: any) { + logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${state}, ${error.stack}`) + } + + try { + logger.info(`Comparing timestamp of /${constants.ON_CONFIRM} and /${constants.ON_STATUS}_${state} API`) + if (_.gte(getValue('onCnfrmtmpstmp'), context.timestamp)) { + businessErrors.tmpstmp1 = `Timestamp for /${constants.ON_CONFIRM} api cannot be greater than or equal to /${constants.ON_STATUS}_${state} api` + } + setValue('tmpstmp', context.timestamp) + } catch (error: any) { + logger.error(`!!Error occurred while comparing timestamp for /${constants.ON_STATUS}_${state}, ${error.stack}`) + } + + const contextTime = context.timestamp + try { + logger.info(`Comparing order.updated_at and context timestamp for /${constants.ON_STATUS}_${state} API`) + + if (!areTimestampsLessThanOrEqualTo(on_status.updated_at, contextTime)) { + businessErrors.tmpstmp2 = `order.updated_at timestamp should be less than or eqaul to context timestamp for /${constants.ON_STATUS}_${state} api` + } + } catch (error: any) { + logger.error( + `!!Error occurred while comparing order updated at for /${constants.ON_STATUS}_${state}, ${error.stack}`, + ) + } + try { + logger.info(`Checking if transaction_id is present in message.order.payment`) + const payment = on_status.payment + const status = payment_status(payment, flow) + if (!status) { + businessErrors['message/order/transaction_id'] = `Transaction_id missing in message/order/payment` + } + } catch (err: any) { + logger.error(`Error while checking transaction is in message.order.payment`) + } + try { + if (flow === FLOW.FLOW012) { + logger.info('Payment status check in on status pending call') + const payment = on_status.payment + if (payment.status !== PAYMENT_STATUS.NOT_PAID) { + logger.error(`Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)`); + businessErrors.pymntstatus = `Payment status should be ${PAYMENT_STATUS.NOT_PAID} for ${FLOW.FLOW012} flow (Cash on Delivery)` + } + } + } catch (err: any) { + logger.error('Error while checking payment in message/order/payment: ' + err.message); + } + if (flow === '6' || flow === '2' || flow === '3' || flow === '5') { + try { + // For Delivery Object + const fulfillments = on_status.fulfillments + if (!fulfillments.length) { + const key = `missingFulfillments` + businessErrors[key] = `missingFulfillments is mandatory for ${ApiSequence.ON_STATUS_PENDING}` + } + else { + const deliveryObjArr = _.filter(fulfillments, { type: "Delivery" }) + if (!deliveryObjArr.length) { + businessErrors[`message/order.fulfillments/`] = `Delivery fullfillment must be present in ${ApiSequence.ON_STATUS_PENDING}` + } + else { + const deliverObj = deliveryObjArr[0] + delete deliverObj?.state + delete deliverObj?.tags + delete deliverObj?.start?.instructions + delete deliverObj?.end?.instructions + fulfillmentsItemsSet.add(deliverObj) + } + } + + } catch (error: any) { + logger.error(`Error while checking Fulfillments Delivery Obj in /${ApiSequence.ON_STATUS_PENDING}, ${error.stack}`) + } + } + try { + const credsWithProviderId = getValue('credsWithProviderId') + const providerId = on_status?.provider?.id + const confirmCreds = on_status?.provider?.creds + const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) + const expectedCreds = found?.creds + if (!expectedCreds) { + businessErrors['MissingCreds'] = + `creds must be present in /${constants.ON_CANCEL} same as in /${constants.ON_SEARCH}` + } + if (flow === FLOW.FLOW017) { + if (!expectedCreds) { + businessErrors['MissingCreds'] = `creds must be present in /${constants.ON_STATUS_PENDING} ` + } else if (!deepCompare(expectedCreds, confirmCreds)) { + businessErrors['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` + } + } + } catch (err: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + } + if (flow === FLOW.FLOW01C) { + const fulfillments = on_status.fulfillments + const deliveryFulfillment = fulfillments.find((f: any) => f.type === 'Delivery') + + if (!deliveryFulfillment.hasOwnProperty('provider_id')) { + businessErrors['missingFulfillments'] = + `provider_id must be present in ${ApiSequence.ON_STATUS_PENDING} as order is accepted` + } + + const id = getProviderId(deliveryFulfillment) + const fulfillmentProviderId = getValue('fulfillmentProviderId') + + if (deliveryFulfillment.hasOwnProperty('provider_id') && id !== fulfillmentProviderId) { + businessErrors['providerIdMismatch'] = + `provider_id in fulfillment in ${ApiSequence.ON_CONFIRM} does not match expected provider_id: expected '${fulfillmentProviderId}' in ${ApiSequence.ON_STATUS_PENDING} but got ${id}` + } + } + + // Validate routing type for retail 1.2.5 + try { + logger.info(`Checking routing type in /${constants.ON_STATUS_PENDING}`) + const onConfirmOrderState = getValue('onCnfrmState') + const storedRoutingType = getValue('routingType') + const domain = getValue('domain') + + // If order state was 'Created' in on_confirm, routing must be provided in on_status_pending + if (onConfirmOrderState === 'Created') { + const routingType = extractRoutingType(on_status.fulfillments) + + if (!routingType) { + businessErrors.routingType = `Routing type tag is mandatory in delivery fulfillment when order.state was 'Created' in /${constants.ON_CONFIRM}` + } else if (!isValidRoutingType(routingType)) { + businessErrors.routingType = `Invalid routing type '${routingType}'. Must be one of: P2P, P2H2P` + } else { + // If routing was already stored (from on_confirm), check for consistency + if (storedRoutingType && storedRoutingType !== routingType) { + businessErrors.routingTypeMismatch = `Routing type mismatch: '${routingType}' in /${constants.ON_STATUS_PENDING} does not match '${storedRoutingType}' from /${constants.ON_CONFIRM}` + } else if (!storedRoutingType) { + // Store routing type for subsequent validations + setValue('routingType', routingType) + logger.info(`Stored routing type: ${routingType} for domain: ${domain}`) + } + } + } + + // If no routing type has been stored yet, use default based on domain + if (!getValue('routingType')) { + const defaultRouting = getDefaultRouting(domain) + setValue('routingType', defaultRouting) + logger.info(`No routing type found, using default: ${defaultRouting} for domain: ${domain}`) + } + } catch (error: any) { + logger.error(`Error while checking routing type in /${constants.ON_STATUS_PENDING}: ${error.stack}`) + } + } catch (err: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + } + } + } catch (error: any) { + console.log(`Error while checking ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING}`, error.message) + } + + // 7. Return logic + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(businessErrors).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors } + } + const combinedErrors = { ...schemaErrors, ...businessErrors } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false + + } catch (error: any) { + const flow = !stateless ? getValue('flow') : 'unknown' + console.log(`Error while validationg flow ${flow} for replacement ${ApiSequence.REPLACEMENT_ON_STATUS_PENDING}`, error.message) + return businessErrors + } +} \ No newline at end of file diff --git a/utils/Retail_.1.2.5/Status/onStatusPicked.ts b/utils/Retail_.1.2.5/Status/onStatusPicked.ts index e168e210..2eef99a9 100644 --- a/utils/Retail_.1.2.5/Status/onStatusPicked.ts +++ b/utils/Retail_.1.2.5/Status/onStatusPicked.ts @@ -17,8 +17,16 @@ import { FLOW } from '../../enum' import { delivery_delay_reasonCodes } from '../../../constants/reasonCode' import { validateStateForRouting } from '../common/routingValidator' -export const checkOnStatusPicked = (data: any, state: string, msgIdSet: any, fulfillmentsItemsSet: any) => { +export const checkOnStatusPicked = ( + data: any, + state: string, + msgIdSet: any, + fulfillmentsItemsSet: any, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const states: string[] = ['Order-picked-up', 'Order-delivered'] const replacementFulfillment = getValue("replacementFulfillment") try { @@ -32,17 +40,27 @@ export const checkOnStatusPicked = (data: any, state: string, msgIdSet: any, ful } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS_PICKED}`) if (msgIdSet.has(context.message_id)) { @@ -53,8 +71,10 @@ export const checkOnStatusPicked = (data: any, state: string, msgIdSet: any, ful logger.error(`!!Error while checking message id for /${constants.ON_STATUS_PICKED}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_PICKED}`, data) @@ -905,6 +925,18 @@ export const checkOnStatusPicked = (data: any, state: string, msgIdSet: any, ful } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) diff --git a/utils/Retail_.1.2.5/Status/onStatusPickupFailed.ts b/utils/Retail_.1.2.5/Status/onStatusPickupFailed.ts index bd5e4fc9..6bc35df2 100644 --- a/utils/Retail_.1.2.5/Status/onStatusPickupFailed.ts +++ b/utils/Retail_.1.2.5/Status/onStatusPickupFailed.ts @@ -20,8 +20,17 @@ import { } from '../common/statusValidationHelpers' import { STATE_TRANSITIONS } from '../../../constants/fulfillmentStates' -export const checkOnStatusPickupFailed = (data: any, _state: string, msgIdSet: any, _fulfillmentsItemsSet: any, flow?: string) => { +export const checkOnStatusPickupFailed = ( + data: any, + _state: string, + msgIdSet: any, + _fulfillmentsItemsSet: any, + flow?: string, + schemaValidation?: boolean, + stateless?: boolean, +) => { const onStatusObj: any = {} + const schemaErrors: any = {} const EXPECTED_STATE = 'Pickup-failed' try { @@ -43,17 +52,27 @@ export const checkOnStatusPickupFailed = (data: any, _state: string, msgIdSet: a } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_STATUS, data) + : 'skip' const contextRes: any = checkContext(context, constants.ON_STATUS) - if (schemaValidation !== 'error') { - Object.assign(onStatusObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusObj, contextRes.ERRORS) } + // Stateless early return with segregated buckets + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + try { logger.info(`Adding Message Id /${constants.ON_STATUS}_${EXPECTED_STATE}`) if (msgIdSet.has(context.message_id)) { @@ -64,8 +83,10 @@ export const checkOnStatusPickupFailed = (data: any, _state: string, msgIdSet: a logger.error(`!!Error while checking message id for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } setValue(`${ApiSequence.ON_STATUS_PICKUP_FAILED}`, data) @@ -318,6 +339,18 @@ export const checkOnStatusPickupFailed = (data: any, _state: string, msgIdSet: a logger.error(`!!Error while storing fulfillment for /${constants.ON_STATUS}_${EXPECTED_STATE}, ${error.stack}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusObj, schemaErrors) + } return onStatusObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS}_${EXPECTED_STATE} API`, JSON.stringify(err.stack)) diff --git a/utils/Retail_.1.2.5/Status/onStatusRTODelivered.ts b/utils/Retail_.1.2.5/Status/onStatusRTODelivered.ts index 59eef671..65e25f5e 100644 --- a/utils/Retail_.1.2.5/Status/onStatusRTODelivered.ts +++ b/utils/Retail_.1.2.5/Status/onStatusRTODelivered.ts @@ -17,8 +17,9 @@ import { } from '../..' import { getValue, setValue } from '../../../shared/dao' -export const checkOnStatusRTODelivered = (data: any) => { +export const checkOnStatusRTODelivered = (data: any, schemaValidation?: boolean, stateless?: boolean) => { const onStatusRtoObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_CANCEL]: 'JSON cannot be empty' } @@ -30,8 +31,9 @@ export const checkOnStatusRTODelivered = (data: any) => { } const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) const flow = getValue('flow') - let schemaValidation: any - schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL_RTO, data) + const schemaValidationResult = schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_CANCEL_RTO, data) + : 'skip' const select: any = getValue(`${ApiSequence.SELECT}`) const contextRes: any = checkContext(context, constants.ON_STATUS) @@ -42,15 +44,25 @@ export const checkOnStatusRTODelivered = (data: any) => { if (checkBap) Object.assign(onStatusRtoObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(onStatusRtoObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(onStatusRtoObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } if (!contextRes?.valid) { Object.assign(onStatusRtoObj, contextRes.ERRORS) } - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onStatusRtoObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onStatusRtoObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } + } + + // Stateless early return + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusRtoObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusRtoObj } } try { @@ -761,6 +773,18 @@ export const checkOnStatusRTODelivered = (data: any) => { logger.error(`!!Error while checking Reason ID ,RTO Id and Initiated_by for ${constants.ON_STATUS_RTO_DELIVERED}`) } + // Bucketed return when flags are provided + if (stateless !== undefined || schemaValidation !== undefined) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onStatusRtoObj).length > 0 + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: onStatusRtoObj } + } + + // Legacy: merge schema errors into main object for validate endpoint + if (Object.keys(schemaErrors).length > 0) { + Object.assign(onStatusRtoObj, schemaErrors) + } return onStatusRtoObj } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_STATUS_RTO_DELIVERED} API`, err) diff --git a/utils/Retail_.1.2.5/Track/onTrack.ts b/utils/Retail_.1.2.5/Track/onTrack.ts index f97b2bb9..75b35c77 100644 --- a/utils/Retail_.1.2.5/Track/onTrack.ts +++ b/utils/Retail_.1.2.5/Track/onTrack.ts @@ -5,8 +5,9 @@ import { logger } from '../../../shared/logger' import { validateSchemaRetailV2, isObjectEmpty, checkContext, checkBppIdOrBapId } from '../..' import { getValue, setValue } from '../../../shared/dao' -export const checkOnTrack = (data: any) => { +export const checkOnTrack = (data: any, schemaValidation?: boolean, stateless?: boolean) => { const onTrckObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.ON_TRACK]: 'JSON cannot be empty' } @@ -17,24 +18,38 @@ export const checkOnTrack = (data: any) => { return { missingFields: '/context, /message, is missing or empty' } } - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2('RET11', constants.ON_TRACK, data) - const select: any = getValue(`${ApiSequence.SELECT}`) + const schemaValidationResult = + schemaValidation !== false ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_TRACK, data) : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + const contextRes: any = checkContext(context, constants.ON_TRACK) + if (!contextRes?.valid) { + Object.assign(onTrckObj, contextRes.ERRORS) + } const checkBap = checkBppIdOrBapId(context.bap_id) const checkBpp = checkBppIdOrBapId(context.bpp_id) if (checkBap) Object.assign(onTrckObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(onTrckObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (schemaValidation !== 'error') { - Object.assign(onTrckObj, schemaValidation) - } - if (!contextRes?.valid) { - Object.assign(onTrckObj, contextRes.ERRORS) + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onTrckObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onTrckObj } + } + const combinedErrors = { ...schemaErrors, ...onTrckObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) + const select: any = getValue(`${ApiSequence.SELECT}`) + setValue(`${ApiSequence.ON_TRACK}`, data) try { @@ -67,8 +82,16 @@ export const checkOnTrack = (data: any) => { ) } - return onTrckObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(onTrckObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: onTrckObj } + } + const combinedErrors = { ...schemaErrors, ...onTrckObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.ON_TRACK} API`, err) + return { error: `Error while checking /${constants.ON_TRACK} API: ${err.message}` } } } diff --git a/utils/Retail_.1.2.5/Track/track.ts b/utils/Retail_.1.2.5/Track/track.ts index 65b497ed..62410a5a 100644 --- a/utils/Retail_.1.2.5/Track/track.ts +++ b/utils/Retail_.1.2.5/Track/track.ts @@ -5,8 +5,9 @@ import { logger } from '../../../shared/logger' import { validateSchemaRetailV2, isObjectEmpty, checkContext, checkBppIdOrBapId } from '../..' import { getValue, setValue } from '../../../shared/dao' -export const checkTrack = (data: any) => { +export const checkTrack = (data: any, schemaValidation?: boolean, stateless?: boolean) => { const trckObj: any = {} + const schemaErrors: any = {} try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.TRACK]: 'JSON cannot be empty' } @@ -17,24 +18,37 @@ export const checkTrack = (data: any) => { return { missingFields: '/context, /message, is missing or empty' } } - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const schemaValidation = validateSchemaRetailV2('RET11', constants.TRACK, data) - const select: any = getValue(`${ApiSequence.SELECT}`) + const schemaValidationResult = + schemaValidation !== false ? validateSchemaRetailV2(context.domain.split(':')[1], constants.TRACK, data) : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) + } + const contextRes: any = checkContext(context, constants.TRACK) + if (!contextRes?.valid) { + Object.assign(trckObj, contextRes.ERRORS) + } const checkBap = checkBppIdOrBapId(context.bap_id) const checkBpp = checkBppIdOrBapId(context.bpp_id) if (checkBap) Object.assign(trckObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(trckObj, { bpp_id: 'context/bpp_id should not be a url' }) - - if (schemaValidation !== 'error') { - Object.assign(trckObj, schemaValidation) + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(trckObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: trckObj } + } + // Merge schema and business errors into one object + const combinedErrors = { ...schemaErrors, ...trckObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } - if (!contextRes?.valid) { - Object.assign(trckObj, contextRes.ERRORS) - } + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) + const select: any = getValue(`${ApiSequence.SELECT}`) setValue(`${ApiSequence.TRACK}`, data) @@ -68,8 +82,16 @@ export const checkTrack = (data: any) => { ) } - return trckObj + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(trckObj).length > 0 + if (!hasSchema && !hasBusiness) return false + if (schemaValidation !== undefined) { + return { schemaErrors, businessErrors: trckObj } + } + const combinedErrors = { ...schemaErrors, ...trckObj } + return Object.keys(combinedErrors).length > 0 ? combinedErrors : false } catch (err: any) { logger.error(`!!Some error occurred while checking /${constants.TRACK} API`, err) + return { error: `Error while checking /${constants.TRACK} API: ${err.message}` } } } diff --git a/utils/Retail_.1.2.5/Update/onUpdate.ts b/utils/Retail_.1.2.5/Update/onUpdate.ts index 7fda7b12..e2518b45 100644 --- a/utils/Retail_.1.2.5/Update/onUpdate.ts +++ b/utils/Retail_.1.2.5/Update/onUpdate.ts @@ -31,8 +31,11 @@ export const checkOnUpdate = ( quoteTrailItemsSet: any, fulfillmentsItemsSet: any, flow: any, + schemaValidation?: boolean, + stateless?: boolean ) => { const onupdtObj: any = {} + const schemaErrors: any = {} const quoteItemSet: any = new Set() const onConfirmQuote = getValue(`${constants.ON_CONFIRM}/quote`) const selectPriceMap: any = getValue('selectPriceMap') @@ -55,10 +58,13 @@ export const checkOnUpdate = ( } // Validating Schema - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_UPDATE, data) + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.ON_UPDATE, data) + : 'skip' - if (schemaValidation !== 'error') { - Object.assign(onupdtObj, schemaValidation) + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } // have to change here as well for flow 6-a and 6-b @@ -109,8 +115,10 @@ export const checkOnUpdate = ( if (checkBap) Object.assign(onupdtObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(onupdtObj, { bpp_id: 'context/bpp_id should not be a url' }) - if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { - onupdtObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + if (!stateless) { + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { + onupdtObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` + } } // Checkinf for valid context object @@ -166,39 +174,42 @@ export const checkOnUpdate = ( logger.error(`Error while checking the payment status`) } - try { - // Comparing the providerId with /select providerId - if (getValue('providerId') != on_update.provider.id) { - onupdtObj.prvdrId = `provider.id mismatches in /${apiSeq} and /${constants.ON_SELECT}` - } - // Comparing the providerLoc with /select providerLoc - if (on_update.provider.locations[0].id != getValue('providerLoc')) { - onupdtObj.prvdrLoc = `provider.locations[0].id mismatches in /${constants.ON_SELECT} and /${apiSeq}` - } - //Comparing the updated_at timestamp with of context.timestamp - if (!_.gte(context.timestamp, on_update.updated_at)) { - onupdtObj[`context/timestamp`] = `context/timestamp should be greater than message/order/updated_at timestamp` + if (!stateless) { + try { + // Comparing the providerId with /select providerId + if (getValue('providerId') != on_update.provider.id) { + onupdtObj.prvdrId = `provider.id mismatches in /${apiSeq} and /${constants.ON_SELECT}` + } + // Comparing the providerLoc with /select providerLoc + if (on_update.provider.locations[0].id != getValue('providerLoc')) { + onupdtObj.prvdrLoc = `provider.locations[0].id mismatches in /${constants.ON_SELECT} and /${apiSeq}` + } + //Comparing the updated_at timestamp with of context.timestamp + if (!_.gte(context.timestamp, on_update.updated_at)) { + onupdtObj[`context/timestamp`] = `context/timestamp should be greater than message/order/updated_at timestamp` + } + } catch (error: any) { + logger.error(`Error while comparing context/timestamp and updated_at timestamp`) } - } catch (error: any) { - logger.error(`Error while comparing context/timestamp and updated_at timestamp`) - } - try { - // Checking for valid item ids in /on_select - const itemsList = message.order.items - let updatedItems: any = getValue('SelectItemList') - - itemsList.forEach((item: any, index: number) => { - if (!updatedItems?.includes(item.id)) { - const key = `inVldItemId[${index}]` - onupdtObj[key] = `Invalid Item Id provided in /${apiSeq}: ${item.id}` - } else if (!updatedItems[item.id] === item.quantity.count) { - const key = `inVldItemCount[${index}]` - onupdtObj[key] = `Count provide for Item Id in /${apiSeq} should be same as /${constants.UPDATE}` - } - }) - } catch (error: any) { - logger.error(`Error while checking for item IDs for /${apiSeq}, ${error.stack}`) + + try { + // Checking for valid item ids in /on_select + const itemsList = message.order.items + let updatedItems: any = getValue('SelectItemList') + + itemsList.forEach((item: any, index: number) => { + if (!updatedItems?.includes(item.id)) { + const key = `inVldItemId[${index}]` + onupdtObj[key] = `Invalid Item Id provided in /${apiSeq}: ${item.id}` + } else if (!updatedItems[item.id] === item.quantity.count) { + const key = `inVldItemCount[${index}]` + onupdtObj[key] = `Count provide for Item Id in /${apiSeq} should be same as /${constants.UPDATE}` + } + }) + } catch (error: any) { + logger.error(`Error while checking for item IDs for /${apiSeq}, ${error.stack}`) + } } //checking for settlement details in payment @@ -246,42 +257,44 @@ export const checkOnUpdate = ( ) } - try { - // Checking for valid item ids inside on_update - const items = on_update.items - const itemSet: any = new Set() - items.forEach((item: any) => { - if (itemSet.has(JSON.stringify(item))) { - onupdtObj[`DuplicateItem[${item.id}]`] = `Duplicate item found in /${apiSeq}` - } else { - itemSet.add(JSON.stringify(item)) // Add the item to the set if it's not already present - } - }) - let updateItemList: any = null - if (flow === '6-a') { - updateItemList = getValue('SelectItemList') - } else { - updateItemList = getValue('updateItemList') - } - if (updateItemList) { + if (!stateless) { + try { + // Checking for valid item ids inside on_update + const items = on_update.items + const itemSet: any = new Set() items.forEach((item: any) => { - if (!updateItemList.includes(item.id)) { - const key = `inVldItemId[${item.id}]` - onupdtObj[key] = `Item ID should be present in /${constants.SELECT} API` + if (itemSet.has(JSON.stringify(item))) { + onupdtObj[`DuplicateItem[${item.id}]`] = `Duplicate item found in /${apiSeq}` } else { - quoteItemSet.add(item.id) - } - }) - on_update.quote.breakup.forEach((item: any) => { - if (!updateItemList.includes(item['@ondc/org/item_id']) && item['"@ondc/org/title_type"'] === 'item') { - const key = `inVldQuoteItemId[${item['@ondc/org/item_id']}]` - onupdtObj[key] = - `Invalid item ID provided in quote object : ${item['@ondc/org/item_id']}should be present in /${constants.UPDATE} API` + itemSet.add(JSON.stringify(item)) // Add the item to the set if it's not already present } }) + let updateItemList: any = null + if (flow === '6-a') { + updateItemList = getValue('SelectItemList') + } else { + updateItemList = getValue('updateItemList') + } + if (updateItemList) { + items.forEach((item: any) => { + if (!updateItemList.includes(item.id)) { + const key = `inVldItemId[${item.id}]` + onupdtObj[key] = `Item ID should be present in /${constants.SELECT} API` + } else { + quoteItemSet.add(item.id) + } + }) + on_update.quote.breakup.forEach((item: any) => { + if (!updateItemList.includes(item['@ondc/org/item_id']) && item['"@ondc/org/title_type"'] === 'item') { + const key = `inVldQuoteItemId[${item['@ondc/org/item_id']}]` + onupdtObj[key] = + `Invalid item ID provided in quote object : ${item['@ondc/org/item_id']}should be present in /${constants.UPDATE} API` + } + }) + } + } catch (error: any) { + logger.error(`Error while checking for item IDs for /${apiSeq}, ${error.stack}`) } - } catch (error: any) { - logger.error(`Error while checking for item IDs for /${apiSeq}, ${error.stack}`) } // try { @@ -362,62 +375,92 @@ export const checkOnUpdate = ( logger.error(`Error while checking for the availability of initiated_by in ${apiSeq}`) } - const flowSixAChecks = (data: any) => { - try { + if (!stateless) { + const flowSixAChecks = (data: any) => { try { - const orderState = getValue('orderState') - if (data.state != orderState) { - onupdtObj[`order.state`] = - `order.state shouldn't be changed from ${orderState} to ${data.state} in /${apiSeq}` + try { + const orderState = getValue('orderState') + if (data.state != orderState) { + onupdtObj[`order.state`] = + `order.state shouldn't be changed from ${orderState} to ${data.state} in /${apiSeq}` + } + } catch { + logger.error(`Error while checkign order.state for the /${apiSeq}`) } - } catch { - logger.error(`Error while checkign order.state for the /${apiSeq}`) - } - try { - setValue(`${ApiSequence.ON_UPDATE_PART_CANCEL}_tmpstmp`, context.timestamp) - } catch (e: any) { - logger.error(`Error while context/timestamp for the /${apiSeq}`) - } + try { + setValue(`${ApiSequence.ON_UPDATE_PART_CANCEL}_tmpstmp`, context.timestamp) + } catch (e: any) { + logger.error(`Error while context/timestamp for the /${apiSeq}`) + } - try { - data.fulfillments.forEach((fulfillment: any) => { - if (fulfillment.type === 'Cancel') { - setValue('cancelFulfillmentID', fulfillment.id) - } - }) - } catch (e: any) { - logger.error(`Error while setting cancelFulfillmentID in /${apiSeq}`) - } + try { + data.fulfillments.forEach((fulfillment: any) => { + if (fulfillment.type === 'Cancel') { + setValue('cancelFulfillmentID', fulfillment.id) + } + }) + } catch (e: any) { + logger.error(`Error while setting cancelFulfillmentID in /${apiSeq}`) + } - // Checking for quote_trail price and item quote price and adding - try { - if (sumQuoteBreakUp(on_update.quote)) { - const price = Number(on_update.quote.price.value) - const priceAtConfirm = Number(getValue('quotePrice')) - const cancelFulfillments = _.filter(on_update.fulfillments, { type: 'Cancel' }) - for (let obj of cancelFulfillments) { - const offerItems = on_update.quote.breakup.find((item: any) => item['@ondc/org/title_type'] === 'offer') - const quoteTrailItems = _.filter(obj.tags, { code: 'quote_trail' }) - const offerBreakup = onConfirmQuote.breakup - .filter((item: any) => item['@ondc/org/title_type'] === 'offer') - .map((item: any) => ({ - id: item['@ondc/org/item_id'], - value: parseFloat(item.price?.value), - })) - console.log('offerBreakupValue', JSON.stringify(offerBreakup)) + // Checking for quote_trail price and item quote price and adding + try { + if (sumQuoteBreakUp(on_update.quote)) { + const price = Number(on_update.quote.price.value) + const priceAtConfirm = Number(getValue('quotePrice')) + const cancelFulfillments = _.filter(on_update.fulfillments, { type: 'Cancel' }) + for (let obj of cancelFulfillments) { + const offerItems = on_update.quote.breakup.find((item: any) => item['@ondc/org/title_type'] === 'offer') + const quoteTrailItems = _.filter(obj.tags, { code: 'quote_trail' }) + const offerBreakup = onConfirmQuote.breakup + .filter((item: any) => item['@ondc/org/title_type'] === 'offer') + .map((item: any) => ({ + id: item['@ondc/org/item_id'], + value: parseFloat(item.price?.value), + })) + console.log('offerBreakupValue', JSON.stringify(offerBreakup)) + + if (offerItems) { + const offerType = offerItems?.item?.tags + ?.find((tag: any) => tag.code === 'offer') + ?.list?.find((entry: any) => entry.code === 'type')?.value + if (offerType === 'buyXgetY') { + const benefitValue = parseInt( + offerItems?.item?.tags + ?.find((tag: any) => tag.code === 'offer') + ?.list?.find((entry: any) => entry.code === 'item_value').value || '0', + ) + if (benefitValue > 0) { + const quoteTrailItemOffer = quoteTrailItems.find((trail) => + trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), + ) + if (quoteTrailItemOffer) { + offerBreakup.forEach((offer: any) => { + const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') + const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') - if (offerItems) { - const offerType = offerItems?.item?.tags - ?.find((tag: any) => tag.code === 'offer') - ?.list?.find((entry: any) => entry.code === 'type')?.value - if (offerType === 'buyXgetY') { - const benefitValue = parseInt( - offerItems?.item?.tags - ?.find((tag: any) => tag.code === 'offer') - ?.list?.find((entry: any) => entry.code === 'item_value').value || '0', - ) - if (benefitValue > 0) { + const actualId = idEntry?.value + const quoteValue = parseFloat(valueEntry?.value || '0') + const expectedValue = Math.abs(offer.value) + + if (actualId !== offer.id) { + onupdtObj['invalidItem'] = `ID : expected '${offer.id}', got '${actualId}'` + } else if (quoteValue < expectedValue) { + onupdtObj['invalidItem'] = + `Value mismatch for ID '${offer.id}': expected ${expectedValue}, got ${quoteValue}` + } + }) + const quoteTrailValue = parseInt( + quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', + ) + console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) + } + + console.log('offerItem', JSON.stringify(offerItems), quoteTrailItemOffer) + } + console.log('benefitValue', benefitValue) + } else { const quoteTrailItemOffer = quoteTrailItems.find((trail) => trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), ) @@ -442,681 +485,427 @@ export const checkOnUpdate = ( ) console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) } - - console.log('offerItem', JSON.stringify(offerItems), quoteTrailItemOffer) - } - console.log('benefitValue', benefitValue) - } else { - const quoteTrailItemOffer = quoteTrailItems.find((trail) => - trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), - ) - if (quoteTrailItemOffer) { - offerBreakup.forEach((offer: any) => { - const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') - const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') - - const actualId = idEntry?.value - const quoteValue = parseFloat(valueEntry?.value || '0') - const expectedValue = Math.abs(offer.value) - - if (actualId !== offer.id) { - onupdtObj['invalidItem'] = `ID : expected '${offer.id}', got '${actualId}'` - } else if (quoteValue < expectedValue) { - onupdtObj['invalidItem'] = - `Value mismatch for ID '${offer.id}': expected ${expectedValue}, got ${quoteValue}` - } - }) - const quoteTrailValue = parseInt( - quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', - ) - console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) } } - } - // if() + // if() - console.log('quoteTrailItems', JSON.stringify(quoteTrailItems)) + console.log('quoteTrailItems', JSON.stringify(quoteTrailItems)) - checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) + checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) + } + logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) + checkQuoteTrailSum(cancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) + } else { + logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) } - logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) - checkQuoteTrailSum(cancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) - } else { - logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) + } catch (error: any) { + logger.error(`Error occurred while checking for quote_trail price and quote breakup price on /${apiSeq}`) } - } catch (error: any) { - logger.error(`Error occurred while checking for quote_trail price and quote breakup price on /${apiSeq}`) - } - // Checking for quoteTrailItems and adding to quoteTrailItemsSet - try { - let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) - if (cancelFulfillmentsArray.length != 0) { - const cancelFulfillments = cancelFulfillmentsArray[0] - const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') - if (quoteTrailItems.length != 0) { - quoteTrailItems.forEach((item: any) => { - quoteTrailItemsSet.add(item) - }) + // Checking for quoteTrailItems and adding to quoteTrailItemsSet + try { + let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) + if (cancelFulfillmentsArray.length != 0) { + const cancelFulfillments = cancelFulfillmentsArray[0] + const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') + if (quoteTrailItems.length != 0) { + quoteTrailItems.forEach((item: any) => { + quoteTrailItemsSet.add(item) + }) + } else { + onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = + `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` + } } else { - onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = - `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` + onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` } - } else { - onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` + } catch (error: any) { + logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) } - } catch (error: any) { - logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) - } - //Reason_id mapping - try { - logger.info(`Reason_id mapping for cancel_request`) - const fulfillments = on_update.fulfillments - let cancelRequestPresent = false - fulfillments.map((fulfillment: any) => { - if (fulfillment.type == 'Cancel') { - const tags = fulfillment.tags - tags.map((tag: any) => { - if (tag.code == 'cancel_request') { - cancelRequestPresent = true - const lists = tag.list - let reason_id = '' - lists.map((list: any) => { - if (list.code == 'reason_id') { - reason_id = list.value - } - if (list.code == 'initiated_by' && list.value !== context.bpp_id) { - onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bpp_id}` - } - if ( - list.code == 'initiated_by' && - list.value === context.bpp_id && - !partcancel_return_reasonCodes.includes(reason_id) - ) { - onupdtObj['invalid_partcancel_return_request_reason'] = - `reason code allowed are ${partcancel_return_reasonCodes}` - } - }) + //Reason_id mapping + try { + logger.info(`Reason_id mapping for cancel_request`) + const fulfillments = on_update.fulfillments + let cancelRequestPresent = false + fulfillments.map((fulfillment: any) => { + if (fulfillment.type == 'Cancel') { + const tags = fulfillment.tags + tags.map((tag: any) => { + if (tag.code == 'cancel_request') { + cancelRequestPresent = true + const lists = tag.list + let reason_id = '' + lists.map((list: any) => { + if (list.code == 'reason_id') { + reason_id = list.value + } + if (list.code == 'initiated_by' && list.value !== context.bpp_id) { + onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bpp_id}` + } + if ( + list.code == 'initiated_by' && + list.value === context.bpp_id && + !partcancel_return_reasonCodes.includes(reason_id) + ) { + onupdtObj['invalid_partcancel_return_request_reason'] = + `reason code allowed are ${partcancel_return_reasonCodes}` + } + }) + } + }) + } + }) + if (!cancelRequestPresent) { + onupdtObj['cancelRequest'] = `Cancel request is not present in the 'Cancel' fulfillment` + } + } catch (error: any) { + logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) + } + // Checking for fulfillmentsItems and adding to fulfillmentsItemsSet + try { + let fulfillmentsArray = JSON.parse(JSON.stringify(on_update.fulfillments)) + if (fulfillmentsArray.length != 0) { + fulfillmentsArray.forEach((ff: any) => { + if (ff.type == 'Cancel') { + fulfillmentsItemsSet.add(ff) } }) + } else { + onupdtObj[`message/order.fulfillments`] = `Fulfillments are missing in ${apiSeq}` } - }) - if (!cancelRequestPresent) { - onupdtObj['cancelRequest'] = `Cancel request is not present in the 'Cancel' fulfillment` - } - } catch (error: any) { - logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) - } - // Checking for fulfillmentsItems and adding to fulfillmentsItemsSet - try { - let fulfillmentsArray = JSON.parse(JSON.stringify(on_update.fulfillments)) - if (fulfillmentsArray.length != 0) { - fulfillmentsArray.forEach((ff: any) => { - if (ff.type == 'Cancel') { - fulfillmentsItemsSet.add(ff) - } - }) - } else { - onupdtObj[`message/order.fulfillments`] = `Fulfillments are missing in ${apiSeq}` + } catch (error: any) { + logger.error(`Error occurred while checking for fulfillments in /${apiSeq}`) } } catch (error: any) { - logger.error(`Error occurred while checking for fulfillments in /${apiSeq}`) + logger.error(`Error while checking the flow 6-a checks in /${apiSeq}`) } - } catch (error: any) { - logger.error(`Error while checking the flow 6-a checks in /${apiSeq}`) } - } - try { - // Checking fulfillment.id, fulfillment.type and tracking - logger.info('Checking fulfillment.id, fulfillment.type and tracking') - on_update.fulfillments.forEach((ff: any) => { - let ffId = '' - let ffType = '' - - if (!ff.id) { - logger.info(`Fulfillment Id must be present `) - onupdtObj['ffId'] = `Fulfillment Id must be present` - } - if (!ff.type) { - logger.info(`Fulfillment Type must be present`) - onupdtObj['ffType'] = `Fulfillment Type must be present` - } + try { + // Checking fulfillment.id, fulfillment.type and tracking + logger.info('Checking fulfillment.id, fulfillment.type and tracking') + on_update.fulfillments.forEach((ff: any) => { + let ffId = '' + let ffType = '' + + if (!ff.id) { + logger.info(`Fulfillment Id must be present `) + onupdtObj['ffId'] = `Fulfillment Id must be present` + } + if (!ff.type) { + logger.info(`Fulfillment Type must be present`) + onupdtObj['ffType'] = `Fulfillment Type must be present` + } - ffType = ff.type - ffId = ff.id + ffType = ff.type + ffId = ff.id - if (ffType != 'Return' && ffType != 'Cancel') { - if (getValue(`${ffId}_tracking`)) { - if (ff.tracking === false || ff.tracking === true) { - if (getValue(`${ffId}_tracking`) != ff.tracking) { - logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) - onupdtObj['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` + if (ffType != 'Return' && ffType != 'Cancel') { + if (getValue(`${ffId}_tracking`)) { + if (ff.tracking === false || ff.tracking === true) { + if (getValue(`${ffId}_tracking`) != ff.tracking) { + logger.info(`Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call`) + onupdtObj['ffTracking'] = `Fulfillment Tracking mismatch with the ${constants.ON_SELECT} call` + } + } else { + logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) + onupdtObj['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` } - } else { - logger.info(`Tracking must be present for fulfillment ID: ${ff.id} in boolean form`) - onupdtObj['ffTracking'] = `Tracking must be present for fulfillment ID: ${ff.id} in boolean form` } } - } - }) - } catch (error: any) { - logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}`) - } + }) + } catch (error: any) { + logger.info(`Error while checking fulfillments id, type and tracking in /${constants.ON_STATUS}`) + } - if (flow === '6-b' || flow === '6-c' || flow === '00B') { - try { - const timestampOnUpdatePartCancel = getValue(`${ApiSequence.ON_UPDATE_PART_CANCEL}_tmpstmp`) - const timeDif = timeDiff(context.timestamp, timestampOnUpdatePartCancel) - if (timeDif <= 0 && (flow !== '00B' || apiSeq !== ApiSequence.ON_UPDATE_REPLACEMENT)) { - const key = 'context/timestamp' - onupdtObj[key] = - `context/timestamp of /${apiSeq} should be greater than /${ApiSequence.ON_UPDATE_PART_CANCEL} context/timestamp` - } + if (flow === '6-b' || flow === '6-c' || flow === '00B') { + try { + const timestampOnUpdatePartCancel = getValue(`${ApiSequence.ON_UPDATE_PART_CANCEL}_tmpstmp`) + const timeDif = timeDiff(context.timestamp, timestampOnUpdatePartCancel) + if (timeDif <= 0 && (flow !== '00B' || apiSeq !== ApiSequence.ON_UPDATE_REPLACEMENT)) { + const key = 'context/timestamp' + onupdtObj[key] = + `context/timestamp of /${apiSeq} should be greater than /${ApiSequence.ON_UPDATE_PART_CANCEL} context/timestamp` + } - const timestamp = getValue('timestamp_') - if (timestamp && timestamp.length != 0) { - const timeDif2 = timeDiff(context.timestamp, timestamp[0]) - if (timeDif2 <= 0) { + const timestamp = getValue('timestamp_') + if (timestamp && timestamp.length != 0) { + const timeDif2 = timeDiff(context.timestamp, timestamp[0]) + if (timeDif2 <= 0) { + const key = 'context/timestamp/' + onupdtObj[key] = + `context/timestamp of /${apiSeq} should be greater than context/timestamp of /${timestamp[1]}` + } + } else { const key = 'context/timestamp/' onupdtObj[key] = - `context/timestamp of /${apiSeq} should be greater than context/timestamp of /${timestamp[1]}` + `context/timestamp of the previous call is missing or the previous action call itself is missing` } - } else { - const key = 'context/timestamp/' - onupdtObj[key] = - `context/timestamp of the previous call is missing or the previous action call itself is missing` + setValue('timestamp_', [context.timestamp, apiSeq]) + } catch (e: any) { + logger.error(`Error while context/timestamp for the /${apiSeq}`) } - setValue('timestamp_', [context.timestamp, apiSeq]) - } catch (e: any) { - logger.error(`Error while context/timestamp for the /${apiSeq}`) - } - try { - const returnFulfillmentArr = _.filter(on_update?.fulfillments, { type: 'Return' }) - function getReturnFfIdAndQuantity(returnFulfillment: any): any { - const ffId = returnFulfillment?.id - if (!ffId) { - onupdtObj['returnFulfillmentId'] = `Fulfillment ID is missing for return fulfillment in ${apiSeq}` - } - let itemQuantity = '' - if (!_.isEmpty(returnFulfillment?.tags)) { - const returnFulifllmentTags = returnFulfillment?.tags[0] - if (!_.isEmpty(returnFulifllmentTags?.list)) { - const returnFulifillmentTagsList = returnFulifllmentTags.list - const replaceObj = _.find(returnFulifillmentTagsList, { code: 'replace' }) - if (replaceObj && replaceObj.value) { - let replaceValue = replaceObj.value - - if (replaceValue === 'yes' || replaceValue === 'no') { - logger.info(`Valid replace value: ${replaceValue} for /${apiSeq}`) + try { + const returnFulfillmentArr = _.filter(on_update?.fulfillments, { type: 'Return' }) + function getReturnFfIdAndQuantity(returnFulfillment: any): any { + const ffId = returnFulfillment?.id + if (!ffId) { + onupdtObj['returnFulfillmentId'] = `Fulfillment ID is missing for return fulfillment in ${apiSeq}` + } + let itemQuantity = '' + if (!_.isEmpty(returnFulfillment?.tags)) { + const returnFulifllmentTags = returnFulfillment?.tags[0] + if (!_.isEmpty(returnFulifllmentTags?.list)) { + const returnFulifillmentTagsList = returnFulifllmentTags.list + const replaceObj = _.find(returnFulifillmentTagsList, { code: 'replace' }) + if (replaceObj && replaceObj.value) { + let replaceValue = replaceObj.value + + if (replaceValue === 'yes' || replaceValue === 'no') { + logger.info(`Valid replace value: ${replaceValue} for /${apiSeq}`) + } } - } - const itemQuantityArr = _.filter(returnFulifillmentTagsList, { code: 'item_quantity' }) + const itemQuantityArr = _.filter(returnFulifillmentTagsList, { code: 'item_quantity' }) - if (itemQuantityArr.length > 0 && itemQuantityArr[0]?.value) { - itemQuantity = itemQuantityArr[0]?.value + if (itemQuantityArr.length > 0 && itemQuantityArr[0]?.value) { + itemQuantity = itemQuantityArr[0]?.value + } else { + onupdtObj['returnFulfillment/code/item_quantity'] = + `Return fulfillment/tags/list/code/item_quantity is missing in ${apiSeq}` + } } else { - onupdtObj['returnFulfillment/code/item_quantity'] = - `Return fulfillment/tags/list/code/item_quantity is missing in ${apiSeq}` + onupdtObj[`returnFulfillment`] = `Return fulfillment/tags/list is missing in ${apiSeq}` } } else { - onupdtObj[`returnFulfillment`] = `Return fulfillment/tags/list is missing in ${apiSeq}` + onupdtObj[`returnFulfillment`] = `Return fulfillment/tags is missing in ${apiSeq}` } - } else { - onupdtObj[`returnFulfillment`] = `Return fulfillment/tags is missing in ${apiSeq}` + return { ffId: ffId, itemQuantity: itemQuantity } } - return { ffId: ffId, itemQuantity: itemQuantity } - } - if (returnFulfillmentArr.length > 0) { - let obj = getReturnFfIdAndQuantity(returnFulfillmentArr[0]) - if (returnFulfillmentArr.length > 1) { - const obj2 = getReturnFfIdAndQuantity(returnFulfillmentArr[1]) - const returnFfIdAndQuantiy: any = getValue(`${ApiSequence.UPDATE_REVERSE_QC}_ffId_itemQuantiy`) - if (obj?.ffId == returnFfIdAndQuantiy?.ffId) { - obj.ffId = obj2?.ffId - obj.itemQuantity = obj2?.itemQuantity + if (returnFulfillmentArr.length > 0) { + let obj = getReturnFfIdAndQuantity(returnFulfillmentArr[0]) + if (returnFulfillmentArr.length > 1) { + const obj2 = getReturnFfIdAndQuantity(returnFulfillmentArr[1]) + const returnFfIdAndQuantiy: any = getValue(`${ApiSequence.UPDATE_REVERSE_QC}_ffId_itemQuantiy`) + if (obj?.ffId == returnFfIdAndQuantiy?.ffId) { + obj.ffId = obj2?.ffId + obj.itemQuantity = obj2?.itemQuantity + } } - } - - let updateReturnFfIdAndQuantiy: any = '' - if (flow === '6-b') { - updateReturnFfIdAndQuantiy = getValue(`${ApiSequence.UPDATE_REVERSE_QC}_ffId_itemQuantiy`) - } else { - updateReturnFfIdAndQuantiy = getValue(`${ApiSequence.UPDATE_LIQUIDATED}_ffId_itemQuantiy`) - } - if (!_.isEmpty(updateReturnFfIdAndQuantiy)) { - if (obj?.ffId && updateReturnFfIdAndQuantiy?.ffId && obj?.ffId != updateReturnFfIdAndQuantiy?.ffId) { - onupdtObj['returnFulfillment/id'] = - `Mismatch occur between the fulfillment Id of ${apiSeq} ${obj?.ffId} and fulfillment Id of ${updateReturnFfIdAndQuantiy?.apiSeq} ${updateReturnFfIdAndQuantiy?.ffId}` + let updateReturnFfIdAndQuantiy: any = '' + if (flow === '6-b') { + updateReturnFfIdAndQuantiy = getValue(`${ApiSequence.UPDATE_REVERSE_QC}_ffId_itemQuantiy`) + } else { + updateReturnFfIdAndQuantiy = getValue(`${ApiSequence.UPDATE_LIQUIDATED}_ffId_itemQuantiy`) } - if ( - obj?.itemQuantity && - updateReturnFfIdAndQuantiy?.itemQuantity && - obj?.itemQuantity != updateReturnFfIdAndQuantiy?.itemQuantity - ) { - onupdtObj['returnFulfillment/itemQuantity'] = - `itemQuantity mismatch between the ${apiSeq} and ${updateReturnFfIdAndQuantiy?.apiSeq}` + + if (!_.isEmpty(updateReturnFfIdAndQuantiy)) { + if (obj?.ffId && updateReturnFfIdAndQuantiy?.ffId && obj?.ffId != updateReturnFfIdAndQuantiy?.ffId) { + onupdtObj['returnFulfillment/id'] = + `Mismatch occur between the fulfillment Id of ${apiSeq} ${obj?.ffId} and fulfillment Id of ${updateReturnFfIdAndQuantiy?.apiSeq} ${updateReturnFfIdAndQuantiy?.ffId}` + } + if ( + obj?.itemQuantity && + updateReturnFfIdAndQuantiy?.itemQuantity && + obj?.itemQuantity != updateReturnFfIdAndQuantiy?.itemQuantity + ) { + onupdtObj['returnFulfillment/itemQuantity'] = + `itemQuantity mismatch between the ${apiSeq} and ${updateReturnFfIdAndQuantiy?.apiSeq}` + } } + } else { + onupdtObj[`returnFulfillment`] = `Return fulfillment is missing in ${apiSeq}` } - } else { - onupdtObj[`returnFulfillment`] = `Return fulfillment is missing in ${apiSeq}` + } catch (e: any) { + logger.error(`Error while returnFulfillment for the /${apiSeq}`) } - } catch (e: any) { - logger.error(`Error while returnFulfillment for the /${apiSeq}`) - } - try { - if (on_update.state != 'Completed') { - onupdtObj[`order.state`] = `Order state should be equal to the 'Completed' in the ${apiSeq}` + try { + if (on_update.state != 'Completed') { + onupdtObj[`order.state`] = `Order state should be equal to the 'Completed' in the ${apiSeq}` + } + } catch (error: any) { + logger.error(`Error while checking order.state for the /${apiSeq}`) } - } catch (error: any) { - logger.error(`Error while checking order.state for the /${apiSeq}`) } - } - if (flow === '6-a') { - flowSixAChecks(message.order) - } - if (flow === '6-b' || flow === '00B') { - // Checking for location and time id is there or not in start/end object of return fulfillment - if ( - apiSeq == ApiSequence.ON_UPDATE_APPROVAL || - apiSeq == ApiSequence.ON_UPDATE_PICKED || - apiSeq == ApiSequence.ON_UPDATE_DELIVERED || - apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT - ) { - try { - // For Return Object - const RETobj: any = _.filter(on_update.fulfillments, { type: 'Return' }) - let ret_start_location: any = {} - if (!RETobj.length) { - logger.error(`Return object is mandatory for ${apiSeq}`) - const key = `missingReturn` - onupdtObj[key] = `Return object is mandatory for ${apiSeq}` - } else { - // Checking for end object inside Return - if (!_.isEmpty(RETobj[0]?.end)) { - const ret_obj_end = RETobj[0]?.end - if (_.isEmpty(ret_obj_end?.location)) { - onupdtObj['Return.end.location'] = `Return fulfillment end location object is missing in ${apiSeq}` - logger.error(`Return fulfillment end location is missing in ${apiSeq}`) - } - if (apiSeq == ApiSequence.ON_UPDATE_DELIVERED) { - if (!_.isEmpty(ret_obj_end?.time)) { - const ret_obj_end_time = ret_obj_end.time - if (!_.isEmpty(ret_obj_end_time?.timestamp)) { - const ret_obj_end_time_timestamp = new Date(ret_obj_end_time.timestamp) - if ( - !(ret_obj_end_time_timestamp instanceof Date) || - ret_obj_end_time_timestamp > new Date(context.timestamp) - ) { + + if (flow === '6-a') { + flowSixAChecks(message.order) + } + if (flow === '6-b' || flow === '00B') { + // Checking for location and time id is there or not in start/end object of return fulfillment + if ( + apiSeq == ApiSequence.ON_UPDATE_APPROVAL || + apiSeq == ApiSequence.ON_UPDATE_PICKED || + apiSeq == ApiSequence.ON_UPDATE_DELIVERED || + apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT + ) { + try { + // For Return Object + const RETobj: any = _.filter(on_update.fulfillments, { type: 'Return' }) + let ret_start_location: any = {} + if (!RETobj.length) { + logger.error(`Return object is mandatory for ${apiSeq}`) + const key = `missingReturn` + onupdtObj[key] = `Return object is mandatory for ${apiSeq}` + } else { + // Checking for end object inside Return + if (!_.isEmpty(RETobj[0]?.end)) { + const ret_obj_end = RETobj[0]?.end + if (_.isEmpty(ret_obj_end?.location)) { + onupdtObj['Return.end.location'] = `Return fulfillment end location object is missing in ${apiSeq}` + logger.error(`Return fulfillment end location is missing in ${apiSeq}`) + } + if (apiSeq == ApiSequence.ON_UPDATE_DELIVERED) { + if (!_.isEmpty(ret_obj_end?.time)) { + const ret_obj_end_time = ret_obj_end.time + if (!_.isEmpty(ret_obj_end_time?.timestamp)) { + const ret_obj_end_time_timestamp = new Date(ret_obj_end_time.timestamp) + if ( + !(ret_obj_end_time_timestamp instanceof Date) || + ret_obj_end_time_timestamp > new Date(context.timestamp) + ) { + const key = 'returnFF/end/time/timestamp' + onupdtObj[key] = + `end/time/timestamp of return fulfillment should be less than or equal to context/timestamp of ${apiSeq}` + } + } else { const key = 'returnFF/end/time/timestamp' - onupdtObj[key] = - `end/time/timestamp of return fulfillment should be less than or equal to context/timestamp of ${apiSeq}` + onupdtObj[key] = `end/time/timestamp of return fulfillment is missing` } } else { const key = 'returnFF/end/time/timestamp' - onupdtObj[key] = `end/time/timestamp of return fulfillment is missing` + onupdtObj[key] = `returnFF/end/time/timestamp of return fulfillment is missing` } - } else { - const key = 'returnFF/end/time/timestamp' - onupdtObj[key] = `returnFF/end/time/timestamp of return fulfillment is missing` - } - } - } else { - onupdtObj['ReturnFulfillment.end'] = `Return fulfillment end object is missing in ${apiSeq}` - } - - // Checking for start object inside Return - if (!_.isEmpty(RETobj[0]?.start)) { - const ret_obj_start = RETobj[0]?.start - if (!_.isEmpty(ret_obj_start?.location)) { - ret_start_location = ret_obj_start.location - if (ret_start_location.id) { - onupdtObj['Return.start.location.id'] = - `Return fulfillment start location id is not required in ${apiSeq}` } } else { - onupdtObj['Return.start.location'] = `Return fulfillment start location object is missing in ${apiSeq}` - logger.error(`Return fulfillment start location is missing in ${apiSeq}`) + onupdtObj['ReturnFulfillment.end'] = `Return fulfillment end object is missing in ${apiSeq}` } - if (!_.isEmpty(ret_obj_start?.time)) { - const ret_obj_start_time = ret_obj_start.time - if (apiSeq == ApiSequence.ON_UPDATE_APPROVAL) { - if (!_.isEmpty(ret_obj_start_time?.range)) { - const ret_obj_start_time_range = ret_obj_start_time?.range - const startTime: any = new Date(ret_obj_start_time_range?.start) - const endTime: any = new Date(ret_obj_start_time_range?.end) - - if (!(startTime instanceof Date) || !(endTime instanceof Date)) { - if (!(startTime instanceof Date)) { - const key = 'returnFF/start/time/range/start' - onupdtObj[key] = - `start/time/range/start of /${apiSeq} should have valid time format for return fulfillment` - } - if (!(endTime instanceof Date)) { - const key = 'returnFF/start/time/range/end' - onupdtObj[key] = - `end/time/range/end of /${apiSeq} should have valid time format for return fulfillment` - } - } else { - const timeDifStart = timeDiff(ret_obj_start_time_range?.start, context.timestamp) - if (timeDifStart < 0) { - const key = 'returnFF/start/time/range/start' - onupdtObj[key] = - `start/time/range/start time of return fulfillment should be greater than context/timestamp of ${apiSeq}` - } - const timeDifEnd = timeDiff(ret_obj_start_time_range?.end, context.timestamp) - if (timeDifEnd <= 0) { - const key = 'returnFF/start/time/range/end' - onupdtObj[key] = - `start/time/range/end time of return fulfillment should be greater than context/timestamp of ${apiSeq}` - } - setValue(`${ApiSequence.ON_UPDATE_APPROVAL}`, { start: startTime, end: endTime }) - if (startTime >= endTime) { - const key = 'returnFF/start/time/range/' - onupdtObj[key] = - `start/time/range/start should not be greater than or equal to start/time/range/end in return fulfillment` - } - } - } else { - onupdtObj['Return.start.time.range'] = - `Return fulfillment start time range object is missing in ${apiSeq}` + // Checking for start object inside Return + if (!_.isEmpty(RETobj[0]?.start)) { + const ret_obj_start = RETobj[0]?.start + if (!_.isEmpty(ret_obj_start?.location)) { + ret_start_location = ret_obj_start.location + if (ret_start_location.id) { + onupdtObj['Return.start.location.id'] = + `Return fulfillment start location id is not required in ${apiSeq}` } } else { - if (!_.isEmpty(ret_obj_start_time?.timestamp)) { - const ret_obj_start_time_timestamp: any = new Date(ret_obj_start_time.timestamp) - const onUpdateApprovalTimeRanges = getValue(`${ApiSequence.ON_UPDATE_APPROVAL}`) - let startTime: any = '' - let endTime: any = '' - if (!isEmpty(onUpdateApprovalTimeRanges)) { - const { start, end }: any = onUpdateApprovalTimeRanges - startTime = start - endTime = end - } - if (!(ret_obj_start_time_timestamp instanceof Date)) { - const key = 'returnFF/start/time/timestamp' - onupdtObj[key] = `start/time/timestamp of return fulfillment should have valid time format` - } else { - if ( - startTime instanceof Date && - endTime instanceof Date && - (ret_obj_start_time_timestamp < startTime || ret_obj_start_time_timestamp > endTime) - ) { - const key = 'returnFF/start/time/timestamp' - onupdtObj[key] = - `start/time/timestamp of return fulfillment should be in the valid time/range as in ${ApiSequence.ON_UPDATE_APPROVAL}` - } - if (ret_obj_start_time_timestamp > context.timestamp) { - const key = 'returnFF/start/time/timestamp/' - onupdtObj[key] = - `start/time/timestamp of return fulfillment should be less than context/timestamp of ${apiSeq}` - } - } - } else { - const key = 'returnFF/start/time/timestamp' - onupdtObj[key] = `start/time/timestamp of return fulfillment is missing` - } + onupdtObj['Return.start.location'] = `Return fulfillment start location object is missing in ${apiSeq}` + logger.error(`Return fulfillment start location is missing in ${apiSeq}`) } - } else { - onupdtObj['Return.start.time'] = `Return fulfillment start time object is missing in ${apiSeq}` - } - } else { - onupdtObj['ReturnFulfillment.start'] = `Return fulfillment start object is missing in ${apiSeq}` - } - } - } catch (error: any) { - logger.error(`Error while checking Fulfillments Return Obj in /${apiSeq}, ${error.stack}`) - } - } - - // Checking for quote_trail price and item quote price - try { - console.log('in this 1') - - if (sumQuoteBreakUp(on_update.quote)) { - const price = Number(on_update.quote.price.value) - const priceAtConfirm = Number(getValue('quotePrice')) - const returnCancelFulfillments = _.filter( - on_update.fulfillments, - (item) => item.type === 'Return' || item.type === 'Cancel', - ) - if (apiSeq == ApiSequence.ON_UPDATE_PICKED || apiSeq == ApiSequence.ON_UPDATE_DELIVERED) { - console.log('in this 2') - const quoteTrailItems = _.filter(returnCancelFulfillments[0].tags, { code: 'quote_trail' }) - console.log('quoteTrailItems', JSON.stringify(quoteTrailItems)) - const offerItems = on_update.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') - const offerBreakup = onConfirmQuote.breakup - .filter((item: any) => item['@ondc/org/title_type'] === 'offer') - .map((item: any) => ({ - id: item['@ondc/org/item_id'], - value: parseFloat(item.price?.value), - })) - console.log('offerBreakupValue', JSON.stringify(offerBreakup)) - - if (offerItems.length > 0) { - offerItems.forEach((offer: any) => { - const offerId = offer['@ondc/org/item_id'] - const updateValue = parseFloat(offer?.price?.value || '0') - const confirmValue = offerBreakup.find((item: any) => item.id === offer['@ondc/org/item_id']).value - - const offerType = offer?.item?.tags - ?.find((tag: any) => tag.code === 'offer') - ?.list?.find((entry: any) => entry.code === 'type')?.value - const quoteTrailItemOffer = quoteTrailItems.find((trail) => - trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), - ) - console.log('quoteTrailItemOffer', quoteTrailItemOffer, confirmValue, updateValue) - if (confirmValue !== updateValue) { - if (offerType === 'buyXgetY' || offerType === 'freebie') { - if (confirmValue > 0) { - if (quoteTrailItemOffer) { - offerBreakup.forEach((offer: any) => { - const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') - const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') - - const actualId = idEntry?.value - const quoteTrailValue = parseFloat(valueEntry?.value || '0') - const expectedMaxValue = Math.abs(offer.value) + if (!_.isEmpty(ret_obj_start?.time)) { + const ret_obj_start_time = ret_obj_start.time + if (apiSeq == ApiSequence.ON_UPDATE_APPROVAL) { + if (!_.isEmpty(ret_obj_start_time?.range)) { + const ret_obj_start_time_range = ret_obj_start_time?.range + const startTime: any = new Date(ret_obj_start_time_range?.start) + const endTime: any = new Date(ret_obj_start_time_range?.end) + + if (!(startTime instanceof Date) || !(endTime instanceof Date)) { + if (!(startTime instanceof Date)) { + const key = 'returnFF/start/time/range/start' + onupdtObj[key] = + `start/time/range/start of /${apiSeq} should have valid time format for return fulfillment` + } + if (!(endTime instanceof Date)) { + const key = 'returnFF/start/time/range/end' + onupdtObj[key] = + `end/time/range/end of /${apiSeq} should have valid time format for return fulfillment` + } + } else { + const timeDifStart = timeDiff(ret_obj_start_time_range?.start, context.timestamp) + if (timeDifStart < 0) { + const key = 'returnFF/start/time/range/start' + onupdtObj[key] = + `start/time/range/start time of return fulfillment should be greater than context/timestamp of ${apiSeq}` + } - if (actualId !== offer.id) { - onupdtObj[`invalidItem_id_${offer.id}`] = - `ID mismatch: expected '${offer.id}', got '${actualId}'` - } else if (quoteTrailValue > expectedMaxValue) { - onupdtObj[`invalidItem_value_${offer.id}`] = - `Returned quote_trail value for offer '${offer.id}' exceeds allowed discount. Max allowed: ${expectedMaxValue}, got: ${quoteTrailValue}` - } - }) - const quoteTrailValue = parseInt( - quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', - ) - console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) + const timeDifEnd = timeDiff(ret_obj_start_time_range?.end, context.timestamp) + if (timeDifEnd <= 0) { + const key = 'returnFF/start/time/range/end' + onupdtObj[key] = + `start/time/range/end time of return fulfillment should be greater than context/timestamp of ${apiSeq}` + } + setValue(`${ApiSequence.ON_UPDATE_APPROVAL}`, { start: startTime, end: endTime }) + if (startTime >= endTime) { + const key = 'returnFF/start/time/range/' + onupdtObj[key] = + `start/time/range/start should not be greater than or equal to start/time/range/end in return fulfillment` + } } - - console.log('offerItem', JSON.stringify(offerItems), quoteTrailItemOffer) + } else { + onupdtObj['Return.start.time.range'] = + `Return fulfillment start time range object is missing in ${apiSeq}` } } else { - const quoteTrailItemOffer = quoteTrailItems.find((trail) => - trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), - ) - if (quoteTrailItemOffer) { - offerBreakup.forEach((offer: any) => { - const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') - const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') - - const actualId = idEntry?.value - const quoteTrailValue = parseFloat(valueEntry?.value || '0') - const expectedMaxValue = Math.abs(offer.value) - - if (actualId !== offer.id) { - onupdtObj[`invalidItem_id_${offer.id}`] = - `ID mismatch: expected '${offer.id}', got '${actualId}'` - } else if (quoteTrailValue > expectedMaxValue) { - onupdtObj[`invalidItem_value_${offer.id}`] = - `Returned quote_trail value for offer '${offer.id}' exceeds allowed discount. Max allowed: ${expectedMaxValue}, got: ${quoteTrailValue}` + if (!_.isEmpty(ret_obj_start_time?.timestamp)) { + const ret_obj_start_time_timestamp: any = new Date(ret_obj_start_time.timestamp) + const onUpdateApprovalTimeRanges = getValue(`${ApiSequence.ON_UPDATE_APPROVAL}`) + let startTime: any = '' + let endTime: any = '' + if (!isEmpty(onUpdateApprovalTimeRanges)) { + const { start, end }: any = onUpdateApprovalTimeRanges + startTime = start + endTime = end + } + if (!(ret_obj_start_time_timestamp instanceof Date)) { + const key = 'returnFF/start/time/timestamp' + onupdtObj[key] = `start/time/timestamp of return fulfillment should have valid time format` + } else { + if ( + startTime instanceof Date && + endTime instanceof Date && + (ret_obj_start_time_timestamp < startTime || ret_obj_start_time_timestamp > endTime) + ) { + const key = 'returnFF/start/time/timestamp' + onupdtObj[key] = + `start/time/timestamp of return fulfillment should be in the valid time/range as in ${ApiSequence.ON_UPDATE_APPROVAL}` } - }) - const quoteTrailValue = parseInt( - quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', - ) - console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) + if (ret_obj_start_time_timestamp > context.timestamp) { + const key = 'returnFF/start/time/timestamp/' + onupdtObj[key] = + `start/time/timestamp of return fulfillment should be less than context/timestamp of ${apiSeq}` + } + } + } else { + const key = 'returnFF/start/time/timestamp' + onupdtObj[key] = `start/time/timestamp of return fulfillment is missing` } } - } else if (confirmValue === updateValue) { - onupdtObj[`unnecessary_quote_trail_${offerId}`] = - `Offer '${offerId}' value has not changed between /on_confirm and /on_update. Hence, remove it from quote_trail.` + } else { + onupdtObj['Return.start.time'] = `Return fulfillment start time object is missing in ${apiSeq}` } - }) - } - - checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) - logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) - checkQuoteTrailSum(returnCancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) - } - } else { - logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) - } - } catch (error: any) { - logger.error(`Error occuerred while checking for quote_trail price and quote breakup price on /${apiSeq}`) - } - - // Checking for quoteTrailItems and adding to quoteTrailItemsSet and comparing with the previous calls - try { - if (flow !== '00B') { - let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) - if (cancelFulfillmentsArray.length != 0) { - const cancelFulfillments = cancelFulfillmentsArray[0] - const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') - if (quoteTrailItems.length != 0) { - if (apiSeq == ApiSequence.ON_UPDATE_INTERIM_REVERSE_QC) { - quoteTrailItems.forEach((item: any) => { - quoteTrailItemsSet.add(item) - }) + } else { + onupdtObj['ReturnFulfillment.start'] = `Return fulfillment start object is missing in ${apiSeq}` } - - quoteTrailItemsSet.forEach((obj1: any) => { - const exist = quoteTrailItems.some((obj2: any) => { - return _.isEqual(obj1, obj2) - }) - if (!exist) { - onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = - 'Missing fulfillments/Cancel/tags/quote_trail as compare to the previous calls' - } - }) - } else { - onupdtObj[`message/ordobj1er.fulfillments/Cancel/tags/quote_trail`] = - `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` } - } else { - onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` + } catch (error: any) { + logger.error(`Error while checking Fulfillments Return Obj in /${apiSeq}, ${error.stack}`) } } - } catch (error: any) { - logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) - } - - //Reason_id mapping - try { - logger.info(`Reason_id mapping for cancel_request`) - const fulfillments = on_update.fulfillments - fulfillments.map((fulfillment: any) => { - if (fulfillment.type !== 'Return') return - - const tags = fulfillment.tags - let returnRequestPresent = false - tags.forEach((tag: any) => { - if (tag.code !== 'return_request') return - returnRequestPresent = true - const lists = tag.list - let reason_id = 'not_found' - - lists.forEach((list: any) => { - if (list.code === 'reason_id') { - reason_id = list.value - } - if (list.code === 'initiated_by') { - if (list.value !== context.bap_id) { - onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bap_id}` - } - - if ( - reason_id !== 'not_found' && - list.value === context.bap_id && - !return_request_reasonCodes.includes(reason_id) - ) { - onupdtObj['invalid_return_request_reason'] = `reason code allowed are ${return_request_reasonCodes}` - } - } - }) - }) - if (!returnRequestPresent) { - onupdtObj['returnRequest'] = `return request is not present in the 'Return' fulfillment` - } - }) - } catch (error: any) { - logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) - } - } - if (flow === '6-c') { - try { - logger.info(`Reason_id mapping for cancel_request`) - const fulfillments = on_update.fulfillments - fulfillments.map((fulfillment: any) => { - if (fulfillment.type !== 'Return') return - const tags = fulfillment.tags - let returnRejectedRequestPresent = false - tags.forEach((tag: any) => { - if (tag.code !== 'return_request') return - returnRejectedRequestPresent = true - const lists = tag.list - let reason_id = '' - - lists.forEach((list: any) => { - if (list.code === 'reason_id') { - reason_id = list.value - } - if (list.code === 'initiated_by') { - if (list.value !== context.bap_id) { - onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bap_id}` - } - if ( - reason_id && - list.value === context.bap_id && - !return_rejected_request_reasonCodes.includes(reason_id) - ) { - onupdtObj['invalid_return_rejected_request_reasonCodes'] = - `reason code allowed are ${return_rejected_request_reasonCodes}` - } - } - }) - }) - if (!returnRejectedRequestPresent) { - onupdtObj['returnRejectedRequest'] = `return rejected request is not present in the 'Return' fulfillment` - } - }) - } catch (error: any) { - logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) - } + // Checking for quote_trail price and item quote price + try { + console.log('in this 1') - // Checking for quote_trail price and item quote price - try { - if (sumQuoteBreakUp(on_update.quote)) { - const price = Number(on_update.quote.price.value) - const priceAtConfirm = Number(getValue('quotePrice')) - const returnCancelFulfillments = _.filter( - on_update.fulfillments, - (item) => item.type === 'Return' || item.type === 'Cancel', - ) - if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED) { - returnCancelFulfillments.forEach((fulfillment: any) => { - const quoteTrailItems = _.filter(fulfillment.tags, { code: 'quote_trail' }) + if (sumQuoteBreakUp(on_update.quote)) { + const price = Number(on_update.quote.price.value) + const priceAtConfirm = Number(getValue('quotePrice')) + const returnCancelFulfillments = _.filter( + on_update.fulfillments, + (item) => item.type === 'Return' || item.type === 'Cancel', + ) + if (apiSeq == ApiSequence.ON_UPDATE_PICKED || apiSeq == ApiSequence.ON_UPDATE_DELIVERED) { + console.log('in this 2') + const quoteTrailItems = _.filter(returnCancelFulfillments[0].tags, { code: 'quote_trail' }) + console.log('quoteTrailItems', JSON.stringify(quoteTrailItems)) const offerItems = on_update.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') const offerBreakup = onConfirmQuote.breakup .filter((item: any) => item['@ondc/org/title_type'] === 'offer') @@ -1167,8 +956,7 @@ export const checkOnUpdate = ( console.log('offerItem', JSON.stringify(offerItems), quoteTrailItemOffer) } - } - else { + } else { const quoteTrailItemOffer = quoteTrailItems.find((trail) => trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), ) @@ -1201,357 +989,584 @@ export const checkOnUpdate = ( } }) } - checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) - }) - logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) - checkQuoteTrailSum(returnCancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) - } - } else { - logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) - } - } catch (error: any) { - logger.error(`Error occuerred while checking for quote_trail price and quote breakup price on /${apiSeq}`) - } - // Checking for quoteTrailItems and adding to quoteTrailItemsSet and comparing with the previous calls - try { - let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) - if (cancelFulfillmentsArray.length != 0) { - const cancelFulfillments = cancelFulfillmentsArray[0] - const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') - if (quoteTrailItems.length != 0) { - if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED) { - quoteTrailItems.forEach((item: any) => { - quoteTrailItemsSet.add(item) - }) + checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) + logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) + checkQuoteTrailSum(returnCancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) } - - quoteTrailItemsSet.forEach((obj1: any) => { - const exist = quoteTrailItems.some((obj2: any) => { - return _.isEqual(obj1, obj2) - }) - if (!exist) { - onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = - 'Missing fulfillments/Cancel/tags/quote_trail as compare to the previous calls' - } - }) } else { - onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = - `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` + logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) } - } else { - onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` + } catch (error: any) { + logger.error(`Error occuerred while checking for quote_trail price and quote breakup price on /${apiSeq}`) } - } catch (error: any) { - logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) - } - } - if (apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT || flow === '00B') { - try { - const RETobj: any = _.filter(on_update.fulfillments, { type: 'Return' }) - console.log('RETobj', RETobj) - const returnFulfillmentState = RETobj[0].state.descriptor.code - console.log('returnFulfillmentState', returnFulfillmentState) - - const updateReplaceValue = getValue('update_replace_value') - console.log('updateReplaceValue', updateReplaceValue) - - if (updateReplaceValue != null) { - if (apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT) { - if (updateReplaceValue === 'yes') { - const returnRequestTag = RETobj[0].tags.find((tag: any) => tag.code === 'return_request') - console.log('returnRequestTag', returnRequestTag) - - if (returnFulfillmentState !== 'Return_Picked') { - onupdtObj['replacement'] = - `Invalid fulfillment state for ${flow} as fulfillment/state/descriptor/code should be 'Return_Picked'` - } - - if (returnRequestTag) { - const replaceEntry = returnRequestTag.list.find((entry: any) => entry.code === 'replace') - const replaceRequestEntry = RETobj[0].tags.find((entry: any) => entry.code === 'replace_request') - - // Validate 'replace' field - if (!replaceEntry) { - onupdtObj['replace_check'] = `'replace' is missing inside return_request tag` - } else if (replaceEntry.value !== updateReplaceValue) { - onupdtObj['replace_check'] = - `'replace' value mismatch inside return_request: expected '${updateReplaceValue}', found '${replaceEntry.value}'` + // Checking for quoteTrailItems and adding to quoteTrailItemsSet and comparing with the previous calls + try { + if (flow !== '00B') { + let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) + if (cancelFulfillmentsArray.length != 0) { + const cancelFulfillments = cancelFulfillmentsArray[0] + const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') + if (quoteTrailItems.length != 0) { + if (apiSeq == ApiSequence.ON_UPDATE_INTERIM_REVERSE_QC) { + quoteTrailItems.forEach((item: any) => { + quoteTrailItemsSet.add(item) + }) } - // Validate 'replace_request' field and corresponding fulfillment - if (!replaceRequestEntry) { - onupdtObj['replace_request'] = `'replace_request' is missing inside replacement fulfillment` - } else { - const replaceFulfillmentId = replaceRequestEntry.list.find((item: any) => item.code === 'id').value - console.log('replaceFulfillmentId', replaceFulfillmentId, JSON.stringify(replaceRequestEntry)) - - const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.id === replaceFulfillmentId) - console.log('deliveryFulfillment', deliveryFulfillment) - - if (!deliveryFulfillment) { - onupdtObj['replace_request'] = - `'replace_request' ID '${replaceFulfillmentId}' not found in fulfillments list` - } else if (deliveryFulfillment.type !== 'Delivery') { - onupdtObj['replace_request'] = - `'replace_request' ID '${replaceFulfillmentId}' is not of type 'Delivery'` - } - try { - setValue(`replacementFulfillment`, deliveryFulfillment) - if (!deliveryFulfillment['@ondc/org/TAT']) { - onupdtObj[`message.order.fulfillments[${deliveryFulfillment.id}]`] = - `'TAT' must be provided in message/order/fulfillments` - } - // Comparing on_confirm delivery fulfillment with on_update replace fulfillment - const ffDesc = deliveryFulfillment.state.descriptor - - const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false - setValue(`ffIdPrecancel`, ffDesc?.code) - if (!ffStateCheck) { - const key = `ffState:fulfillment[id]:${deliveryFulfillment.id}` - onupdtObj[key] = `default fulfillments state is missing in /${constants.ON_UPDATE_REPLACEMENT}` - } - - if (!deliveryFulfillment.start || !deliveryFulfillment.end) { - onupdtObj.ffstartend = `fulfillments start and end locations are mandatory for fulfillment id: ${deliveryFulfillment.id}` - } - - try { - if (!compareCoordinates(deliveryFulfillment.start.location.gps, getValue('providerGps'))) { - onupdtObj.sellerGpsErr = `store gps location /fulfillments/:${deliveryFulfillment.id}/start/location/gps can't change` - } - } catch (error: any) { - logger.error( - `!!Error while checking store location in /${constants.ON_UPDATE_REPLACEMENT}, ${error.stack}`, - ) - } - - try { - if (!getValue('providerName')) { - onupdtObj.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_UPDATE_REPLACEMENT}` - } else if ( - !_.isEqual(deliveryFulfillment.start.location.descriptor.name, getValue('providerName')) - ) { - onupdtObj.sellerNameErr = `store name /fulfillments/start/location/descriptor/name can't change with fulfillment id: ${deliveryFulfillment.id}` - } - } catch (error: any) { - logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) - } - - if (!_.isEqual(deliveryFulfillment.end.location.gps, getValue('buyerGps'))) { - onupdtObj.buyerGpsErr = `fulfillments.end.location gps with id ${deliveryFulfillment.id} is not matching with gps in /on_conifrm` - } - - if (!_.isEqual(deliveryFulfillment.end.location.address.area_code, getValue('buyerAddr'))) { - onupdtObj.gpsErr = `fulfillments.end.location.address.area_code with id ${deliveryFulfillment.id} is not matching with area_code in /on_confirm` - } - } catch (error: any) { - logger.error( - `Error while comparing fulfillment obj ${apiSeq.ON_UPDATE_REPLACEMENT} and ${apiSeq.ON_CONFIRM}`, - error.stack, - ) + quoteTrailItemsSet.forEach((obj1: any) => { + const exist = quoteTrailItems.some((obj2: any) => { + return _.isEqual(obj1, obj2) + }) + if (!exist) { + onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = + 'Missing fulfillments/Cancel/tags/quote_trail as compare to the previous calls' } - } + }) } else { - onupdtObj['replace_check'] = `'return_request' tag is missing in tags` + onupdtObj[`message/ordobj1er.fulfillments/Cancel/tags/quote_trail`] = + `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` } } else { - onupdtObj['rplcmnt'] = - `Replacement not possible as expected value for replace is yes but got ${updateReplaceValue}` + onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` } } + } catch (error: any) { + logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) } - } catch (error: any) { - console.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) - } - } - const fulfillmentId = getValue('fulfillmentId') - try { - const res = on_update.fulfillments.find((ele: { id: any }) => ele.id === fulfillmentId) - const updateObj = getValue('updateObj') - if (!deepCompare(updateObj, res)) { - onupdtObj['addressUpdate'] = `Address must be updated which is given in /${constants.UPDATE}` - } - } catch (error) { - logger.error(`Error while checking for update_target_obj for /${apiSeq} , ${error}`) - } - try { - const flowChecks: Record = { - [FLOW.FLOW00F]: { - key: 'addressUpdate', - message: `Address must be same as given in /${constants.UPDATE}`, - }, - [FLOW.FLOW011]: { - key: 'buyerInstructions', - message: `Instructions must be same as given in /${constants.UPDATE}`, - }, - } + //Reason_id mapping + try { + logger.info(`Reason_id mapping for cancel_request`) + const fulfillments = on_update.fulfillments + fulfillments.map((fulfillment: any) => { + if (fulfillment.type !== 'Return') return + + const tags = fulfillment.tags + let returnRequestPresent = false + tags.forEach((tag: any) => { + if (tag.code !== 'return_request') return + returnRequestPresent = true + const lists = tag.list + let reason_id = 'not_found' + + lists.forEach((list: any) => { + if (list.code === 'reason_id') { + reason_id = list.value + } + if (list.code === 'initiated_by') { + if (list.value !== context.bap_id) { + onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bap_id}` + } - if (flow in flowChecks) { - const res = on_update.fulfillments.find((ele: { id: any }) => ele.id === fulfillmentId) - const updateObj = getValue('updateObj') - const { key, message } = flowChecks[flow] - if (!deepCompare(updateObj, res)) { - onupdtObj[key] = message + if ( + reason_id !== 'not_found' && + list.value === context.bap_id && + !return_request_reasonCodes.includes(reason_id) + ) { + onupdtObj['invalid_return_request_reason'] = `reason code allowed are ${return_request_reasonCodes}` + } + } + }) + }) + if (!returnRequestPresent) { + onupdtObj['returnRequest'] = `return request is not present in the 'Return' fulfillment` + } + }) + } catch (error: any) { + logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) } } - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) - } + if (flow === '6-c') { + try { + logger.info(`Reason_id mapping for cancel_request`) + const fulfillments = on_update.fulfillments + fulfillments.map((fulfillment: any) => { + if (fulfillment.type !== 'Return') return + + const tags = fulfillment.tags + let returnRejectedRequestPresent = false + tags.forEach((tag: any) => { + if (tag.code !== 'return_request') return + returnRejectedRequestPresent = true + const lists = tag.list + let reason_id = '' + + lists.forEach((list: any) => { + if (list.code === 'reason_id') { + reason_id = list.value + } + if (list.code === 'initiated_by') { + if (list.value !== context.bap_id) { + onupdtObj['invalid_initiated_by'] = `initiated_by should be ${context.bap_id}` + } + if ( + reason_id && + list.value === context.bap_id && + !return_rejected_request_reasonCodes.includes(reason_id) + ) { + onupdtObj['invalid_return_rejected_request_reasonCodes'] = + `reason code allowed are ${return_rejected_request_reasonCodes}` + } + } + }) + }) + if (!returnRejectedRequestPresent) { + onupdtObj['returnRejectedRequest'] = `return rejected request is not present in the 'Return' fulfillment` + } + }) + } catch (error: any) { + logger.error(`!!Error while mapping cancellation_reason_id in ${apiSeq}`) + } - try { - const credsWithProviderId = getValue('credsWithProviderId') - const providerId = on_update?.provider?.id - const confirmCreds = on_update?.provider?.creds - const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) - const expectedCreds = found?.creds - if (!expectedCreds && confirmCreds) { - onupdtObj['MissingCreds'] = `creds must be same as in /${constants.ON_SEARCH}` + // Checking for quote_trail price and item quote price + try { + if (sumQuoteBreakUp(on_update.quote)) { + const price = Number(on_update.quote.price.value) + const priceAtConfirm = Number(getValue('quotePrice')) + const returnCancelFulfillments = _.filter( + on_update.fulfillments, + (item) => item.type === 'Return' || item.type === 'Cancel', + ) + if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED) { + returnCancelFulfillments.forEach((fulfillment: any) => { + const quoteTrailItems = _.filter(fulfillment.tags, { code: 'quote_trail' }) + const offerItems = on_update.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') + const offerBreakup = onConfirmQuote.breakup + .filter((item: any) => item['@ondc/org/title_type'] === 'offer') + .map((item: any) => ({ + id: item['@ondc/org/item_id'], + value: parseFloat(item.price?.value), + })) + console.log('offerBreakupValue', JSON.stringify(offerBreakup)) + + if (offerItems.length > 0) { + offerItems.forEach((offer: any) => { + const offerId = offer['@ondc/org/item_id'] + const updateValue = parseFloat(offer?.price?.value || '0') + const confirmValue = offerBreakup.find((item: any) => item.id === offer['@ondc/org/item_id']).value + + const offerType = offer?.item?.tags + ?.find((tag: any) => tag.code === 'offer') + ?.list?.find((entry: any) => entry.code === 'type')?.value + const quoteTrailItemOffer = quoteTrailItems.find((trail) => + trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), + ) + console.log('quoteTrailItemOffer', quoteTrailItemOffer, confirmValue, updateValue) + if (confirmValue !== updateValue) { + if (offerType === 'buyXgetY' || offerType === 'freebie') { + if (confirmValue > 0) { + if (quoteTrailItemOffer) { + offerBreakup.forEach((offer: any) => { + const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') + const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') + + const actualId = idEntry?.value + const quoteTrailValue = parseFloat(valueEntry?.value || '0') + const expectedMaxValue = Math.abs(offer.value) + + if (actualId !== offer.id) { + onupdtObj[`invalidItem_id_${offer.id}`] = + `ID mismatch: expected '${offer.id}', got '${actualId}'` + } else if (quoteTrailValue > expectedMaxValue) { + onupdtObj[`invalidItem_value_${offer.id}`] = + `Returned quote_trail value for offer '${offer.id}' exceeds allowed discount. Max allowed: ${expectedMaxValue}, got: ${quoteTrailValue}` + } + }) + const quoteTrailValue = parseInt( + quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', + ) + console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) + } + + console.log('offerItem', JSON.stringify(offerItems), quoteTrailItemOffer) + } + } + else { + const quoteTrailItemOffer = quoteTrailItems.find((trail) => + trail.list.some((entry: any) => entry.code === 'type' && entry.value === 'offer'), + ) + if (quoteTrailItemOffer) { + offerBreakup.forEach((offer: any) => { + const idEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'id') + const valueEntry = quoteTrailItemOffer.list.find((item: any) => item.code === 'value') + + const actualId = idEntry?.value + const quoteTrailValue = parseFloat(valueEntry?.value || '0') + const expectedMaxValue = Math.abs(offer.value) + + if (actualId !== offer.id) { + onupdtObj[`invalidItem_id_${offer.id}`] = + `ID mismatch: expected '${offer.id}', got '${actualId}'` + } else if (quoteTrailValue > expectedMaxValue) { + onupdtObj[`invalidItem_value_${offer.id}`] = + `Returned quote_trail value for offer '${offer.id}' exceeds allowed discount. Max allowed: ${expectedMaxValue}, got: ${quoteTrailValue}` + } + }) + const quoteTrailValue = parseInt( + quoteTrailItemOffer.list.find((entry: any) => entry.code === 'value')?.value || '0', + ) + console.log('quoteTrailValue', quoteTrailValue, quoteTrailItemOffer) + } + } + } else if (confirmValue === updateValue) { + onupdtObj[`unnecessary_quote_trail_${offerId}`] = + `Offer '${offerId}' value has not changed between /on_confirm and /on_update. Hence, remove it from quote_trail.` + } + }) + } + checkQuoteTrail(quoteTrailItems, onupdtObj, selectPriceMap, itemSet) + }) + logger.info(`Checking for quote_trail price and item quote price sum for ${apiSeq}`) + checkQuoteTrailSum(returnCancelFulfillments, price, priceAtConfirm, onupdtObj, ApiSequence.ON_UPDATE) + } + } else { + logger.error(`The price breakdown in brakup does not match with the total_price for ${apiSeq} `) + } + } catch (error: any) { + logger.error(`Error occuerred while checking for quote_trail price and quote breakup price on /${apiSeq}`) + } + + // Checking for quoteTrailItems and adding to quoteTrailItemsSet and comparing with the previous calls + try { + let cancelFulfillmentsArray = _.filter(on_update.fulfillments, { type: 'Cancel' }) + if (cancelFulfillmentsArray.length != 0) { + const cancelFulfillments = cancelFulfillmentsArray[0] + const quoteTrailItems = cancelFulfillments.tags.filter((tag: any) => tag.code == 'quote_trail') + if (quoteTrailItems.length != 0) { + if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED) { + quoteTrailItems.forEach((item: any) => { + quoteTrailItemsSet.add(item) + }) + } + + quoteTrailItemsSet.forEach((obj1: any) => { + const exist = quoteTrailItems.some((obj2: any) => { + return _.isEqual(obj1, obj2) + }) + if (!exist) { + onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = + 'Missing fulfillments/Cancel/tags/quote_trail as compare to the previous calls' + } + }) + } else { + onupdtObj[`message/order.fulfillments/Cancel/tags/quote_trail`] = + `Fulfillments/Cancel/tags/quote_trail is missing in ${apiSeq}` + } + } else { + onupdtObj[`message/order.fulfillments/Cancel`] = `Fulfillments/Cancel is missing in ${apiSeq}` + } + } catch (error: any) { + logger.error(`Error occurred while checking for quote_trail in /${apiSeq}`) + } } - if (flow === FLOW.FLOW017) { - if (!expectedCreds && confirmCreds) { - onupdtObj['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` - } else if (!deepCompare(expectedCreds, confirmCreds)) { - onupdtObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` + if (apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT || flow === '00B') { + try { + const RETobj: any = _.filter(on_update.fulfillments, { type: 'Return' }) + console.log('RETobj', RETobj) + + const returnFulfillmentState = RETobj[0].state.descriptor.code + console.log('returnFulfillmentState', returnFulfillmentState) + + const updateReplaceValue = getValue('update_replace_value') + console.log('updateReplaceValue', updateReplaceValue) + + if (updateReplaceValue != null) { + if (apiSeq === ApiSequence.ON_UPDATE_REPLACEMENT) { + if (updateReplaceValue === 'yes') { + const returnRequestTag = RETobj[0].tags.find((tag: any) => tag.code === 'return_request') + console.log('returnRequestTag', returnRequestTag) + + if (returnFulfillmentState !== 'Return_Picked') { + onupdtObj['replacement'] = + `Invalid fulfillment state for ${flow} as fulfillment/state/descriptor/code should be 'Return_Picked'` + } + + if (returnRequestTag) { + const replaceEntry = returnRequestTag.list.find((entry: any) => entry.code === 'replace') + const replaceRequestEntry = RETobj[0].tags.find((entry: any) => entry.code === 'replace_request') + + // Validate 'replace' field + if (!replaceEntry) { + onupdtObj['replace_check'] = `'replace' is missing inside return_request tag` + } else if (replaceEntry.value !== updateReplaceValue) { + onupdtObj['replace_check'] = + `'replace' value mismatch inside return_request: expected '${updateReplaceValue}', found '${replaceEntry.value}'` + } + + // Validate 'replace_request' field and corresponding fulfillment + if (!replaceRequestEntry) { + onupdtObj['replace_request'] = `'replace_request' is missing inside replacement fulfillment` + } else { + const replaceFulfillmentId = replaceRequestEntry.list.find((item: any) => item.code === 'id').value + console.log('replaceFulfillmentId', replaceFulfillmentId, JSON.stringify(replaceRequestEntry)) + + const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.id === replaceFulfillmentId) + console.log('deliveryFulfillment', deliveryFulfillment) + + if (!deliveryFulfillment) { + onupdtObj['replace_request'] = + `'replace_request' ID '${replaceFulfillmentId}' not found in fulfillments list` + } else if (deliveryFulfillment.type !== 'Delivery') { + onupdtObj['replace_request'] = + `'replace_request' ID '${replaceFulfillmentId}' is not of type 'Delivery'` + } + try { + setValue(`replacementFulfillment`, deliveryFulfillment) + if (!deliveryFulfillment['@ondc/org/TAT']) { + onupdtObj[`message.order.fulfillments[${deliveryFulfillment.id}]`] = + `'TAT' must be provided in message/order/fulfillments` + } + // Comparing on_confirm delivery fulfillment with on_update replace fulfillment + const ffDesc = deliveryFulfillment.state.descriptor + + const ffStateCheck = ffDesc.hasOwnProperty('code') ? ffDesc.code === 'Pending' : false + setValue(`ffIdPrecancel`, ffDesc?.code) + if (!ffStateCheck) { + const key = `ffState:fulfillment[id]:${deliveryFulfillment.id}` + onupdtObj[key] = `default fulfillments state is missing in /${constants.ON_UPDATE_REPLACEMENT}` + } + + if (!deliveryFulfillment.start || !deliveryFulfillment.end) { + onupdtObj.ffstartend = `fulfillments start and end locations are mandatory for fulfillment id: ${deliveryFulfillment.id}` + } + + try { + if (!compareCoordinates(deliveryFulfillment.start.location.gps, getValue('providerGps'))) { + onupdtObj.sellerGpsErr = `store gps location /fulfillments/:${deliveryFulfillment.id}/start/location/gps can't change` + } + } catch (error: any) { + logger.error( + `!!Error while checking store location in /${constants.ON_UPDATE_REPLACEMENT}, ${error.stack}`, + ) + } + + try { + if (!getValue('providerName')) { + onupdtObj.sellerNameErr = `Invalid store name inside fulfillments in /${constants.ON_UPDATE_REPLACEMENT}` + } else if ( + !_.isEqual(deliveryFulfillment.start.location.descriptor.name, getValue('providerName')) + ) { + onupdtObj.sellerNameErr = `store name /fulfillments/start/location/descriptor/name can't change with fulfillment id: ${deliveryFulfillment.id}` + } + } catch (error: any) { + logger.error(`!!Error while checking store name in /${constants.ON_CONFIRM}`) + } + + if (!_.isEqual(deliveryFulfillment.end.location.gps, getValue('buyerGps'))) { + onupdtObj.buyerGpsErr = `fulfillments.end.location gps with id ${deliveryFulfillment.id} is not matching with gps in /on_conifrm` + } + + if (!_.isEqual(deliveryFulfillment.end.location.address.area_code, getValue('buyerAddr'))) { + onupdtObj.gpsErr = `fulfillments.end.location.address.area_code with id ${deliveryFulfillment.id} is not matching with area_code in /on_confirm` + } + } catch (error: any) { + logger.error( + `Error while comparing fulfillment obj ${apiSeq.ON_UPDATE_REPLACEMENT} and ${apiSeq.ON_CONFIRM}`, + error.stack, + ) + } + } + } else { + onupdtObj['replace_check'] = `'return_request' tag is missing in tags` + } + } else { + onupdtObj['rplcmnt'] = + `Replacement not possible as expected value for replace is yes but got ${updateReplaceValue}` + } + } + } + } catch (error: any) { + console.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) } } - } catch (err: any) { - logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) - } - try { - const previousToken = getValue('otpToken') - const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.type === "Delivery") - // Will use this in future - const delivery_instructions = deliveryFulfillment.end.instructions - if (flow === FLOW.FLOW010) { - if (!deliveryFulfillment || !deliveryFulfillment.end) { - onupdtObj['dlvryFulfillment'] = `Missing delivery fulfillment details for flow: ${flow}` - } else { + const fulfillmentId = getValue('fulfillmentId') + + try { + const res = on_update.fulfillments.find((ele: { id: any }) => ele.id === fulfillmentId) + const updateObj = getValue('updateObj') + if (!deepCompare(updateObj, res)) { + onupdtObj['addressUpdate'] = `Address must be updated which is given in /${constants.UPDATE}` + } + } catch (error) { + logger.error(`Error while checking for update_target_obj for /${apiSeq} , ${error}`) + } + try { + const flowChecks: Record = { + [FLOW.FLOW00F]: { + key: 'addressUpdate', + message: `Address must be same as given in /${constants.UPDATE}`, + }, + [FLOW.FLOW011]: { + key: 'buyerInstructions', + message: `Instructions must be same as given in /${constants.UPDATE}`, + }, + } - // Check if delivery instructions exist - if (!delivery_instructions) { - onupdtObj['dlvryInstructions'] = `Delivery instructions are required for flow: ${flow}` + if (flow in flowChecks) { + const res = on_update.fulfillments.find((ele: { id: any }) => ele.id === fulfillmentId) + const updateObj = getValue('updateObj') + const { key, message } = flowChecks[flow] + if (!deepCompare(updateObj, res)) { + onupdtObj[key] = message + } + } + } catch (err: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + } + + try { + const credsWithProviderId = getValue('credsWithProviderId') + const providerId = on_update?.provider?.id + const confirmCreds = on_update?.provider?.creds + const found = credsWithProviderId.find((ele: { providerId: any }) => ele.providerId === providerId) + const expectedCreds = found?.creds + if (!expectedCreds && confirmCreds) { + onupdtObj['MissingCreds'] = `creds must be same as in /${constants.ON_SEARCH}` + } + if (flow === FLOW.FLOW017) { + if (!expectedCreds && confirmCreds) { + onupdtObj['MissingCreds'] = `creds must be present in /${constants.ON_SEARCH}` + } else if (!deepCompare(expectedCreds, confirmCreds)) { + onupdtObj['MissingCreds'] = `creds must be present and same as in /${constants.ON_SEARCH}` + } + } + } catch (err: any) { + logger.error(`!!Some error occurred while checking /${constants.ON_STATUS} API`, err) + } + try { + const previousToken = getValue('otpToken') + const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.type === "Delivery") + // Will use this in future + const delivery_instructions = deliveryFulfillment.end.instructions + if (flow === FLOW.FLOW010) { + if (!deliveryFulfillment || !deliveryFulfillment.end) { + onupdtObj['dlvryFulfillment'] = `Missing delivery fulfillment details for flow: ${flow}` } else { - // Validate instruction code - const instructionCode = delivery_instructions.code - const isValidCode = Object.values(buyer_instructions).includes(instructionCode) - if (!isValidCode) { - onupdtObj['instructionCodeInvld'] = `Delivery instruction code '${instructionCode}' is invalid` - } - const instructionCodeValue = delivery_instructions.short_desc - - // If code is "OTP", validate authorization - if (instructionCode === buyer_instructions.CODE1) { - const authorization = deliveryFulfillment.end.authorization - validateAuthorization(authorization, onupdtObj, 'authorization') - const updatedToken = authorization['token'] - if(previousToken == updatedToken){ - onupdtObj["invldToken"] = `Otp should be different for the flow: ${flow}` + // Check if delivery instructions exist + if (!delivery_instructions) { + onupdtObj['dlvryInstructions'] = `Delivery instructions are required for flow: ${flow}` + } else { + // Validate instruction code + const instructionCode = delivery_instructions.code + const isValidCode = Object.values(buyer_instructions).includes(instructionCode) + + if (!isValidCode) { + onupdtObj['instructionCodeInvld'] = `Delivery instruction code '${instructionCode}' is invalid` } - } - else{ - if(previousToken === instructionCodeValue){ - onupdtObj["invldToken"] = `Otp should be different for the flow: ${flow}` + const instructionCodeValue = delivery_instructions.short_desc + + // If code is "OTP", validate authorization + if (instructionCode === buyer_instructions.CODE1) { + const authorization = deliveryFulfillment.end.authorization + validateAuthorization(authorization, onupdtObj, 'authorization') + const updatedToken = authorization['token'] + if (previousToken == updatedToken) { + onupdtObj["invldToken"] = `Otp should be different for the flow: ${flow}` + } + } + else { + if (previousToken === instructionCodeValue) { + onupdtObj["invldToken"] = `Otp should be different for the flow: ${flow}` + } } } } } + } catch (error: any) { + console.error('Error while validating delivery instructions:', error.message) } - } catch (error: any) { - console.error('Error while validating delivery instructions:', error.message) - } - try { - if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED || apiSeq === ApiSequence.ON_UPDATE_PICKED) { - // let updatedItemID = getValue('updatedItemID') - // console.log('updatedItemID', updatedItemID) - // const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) - // const offerItem = on_update.quote.breakup.filter((item: any) => item["@ondc/org/title_type"] === 'offer') - // const onConfirmOfferItem = onConfirmQuote.breakup.find((item: any) => item['@ondc/org/title_type'] === 'offer') - // console.log('offerItem', offerItem) - // let offerApplied - // if (offerItem.length > 0) { - // let offerItemIds; - // offerItem.forEach((offer: any,index:number) => { - // offerApplied = providerOffers.find((item: any) => item.id === offer["@ondc/org/item_id"]) - // if (offerApplied) { - - // offerItemIds = offerApplied?.item_ids || [] - // const matchingItems = offerItemIds.find((id: string) => updatedItemID.includes(id)) || [] - // const match = onConfirmOfferItem.find((id:any)=>id === offer["@ondc/org/item_id"]) - // console.log('matchingItems', JSON.stringify(matchingItems)) - - // if (matchingItems.length === 0 || !matchingItems) { - // onupdtObj[`offer_item[${index}]`] = - // `Offer with id '${offer["@ondc/org/item_id"]}' is not applicable for any of the ordered item(s). \nApplicable items in offer: [${offerItemIds.join(', ')}], \nItems in order: [${offerItemIds.join(', ')}].` - // return - // } - // console.log('offer applied', JSON.stringify(offerApplied)) - // } - // }) - // } - const offerItem = on_update.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') - const deliveryCharges = Math.abs(parseFloat(on_update.quote.breakup.find((item:any)=>item["@ondc/org/title_type"] === "delivery")?.price?.value)) || 0; - const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) - let offerApplied: any - console.log('applied ', offerApplied) - - const totalWithoutOffers = on_update?.quote?.breakup.reduce((sum: any, item: any) => { - if (item['@ondc/org/title_type'] !== 'offer') { - const value = parseFloat(item.price?.value || '0') - return sum + value - } - return sum - }, 0) - console.log("totalWithoutOffers",totalWithoutOffers); - - - const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.type === "Delivery"); - console.log('deliveryFulfillment', deliveryFulfillment); - - const orderItemIds = on_update?.items - ?.filter((item: any) => item.fulfillment_id === deliveryFulfillment?.id) - ?.map((item: any) => ({ - id: item.id, - fulfillment_id: item.fulfillment_id, - })) || []; - const items: any = orderItemIds - .map((id: any) => { - const item = on_update?.quote?.breakup.find((entry: any) => entry['@ondc/org/item_id'] === id) - return item ? { id, price: item.price.value, quantity: item['@ondc/org/item_quantity']?.count } : null - }) - .filter((item: any) => item !== null) - console.log('itemPrices of found items in breakup', JSON.stringify(items)) - - const priceSums = items.reduce((acc: Record, item: { id: string; price: string }) => { - const { id, price } = item - acc[id] = (acc[id] || 0) + parseFloat(price) - return acc - }, {}) - console.log("priceSums",priceSums); - let offerPriceValue: number = 0 - let offerItemQuantity:number = 0 - console.log("offerItemQuantity",offerItemQuantity); - - if (offerItem.length > 0) { - offerItem.forEach((offer: any,i:number) => { - offerApplied = providerOffers.find((item: any) => item.id === offer['@ondc/org/item_id']) - const offerId = offer['@ondc/org/item_id'] - offerPriceValue = Math.abs(parseFloat(offer?.price?.value)) - const offerType = offer?.item?.tags - ?.find((tag: any) => tag.code === 'offer') - ?.list?.find((entry: any) => entry.code === 'type')?.value - if (offerApplied) { - console.log("offerId",offerId); - - const offerItemIds = offerApplied?.item_ids || [] - - const orderItemIdList = orderItemIds.map((item: any) => item.id) // extract just the IDs - - const matchingItems = offerItemIds.filter((id: string) => orderItemIdList.includes(id)) || [] - console.log("matchingItems",JSON.stringify(matchingItems)); + try { + if (apiSeq == ApiSequence.ON_UPDATE_LIQUIDATED || apiSeq === ApiSequence.ON_UPDATE_PICKED) { + // let updatedItemID = getValue('updatedItemID') + // console.log('updatedItemID', updatedItemID) + // const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) + // const offerItem = on_update.quote.breakup.filter((item: any) => item["@ondc/org/title_type"] === 'offer') + // const onConfirmOfferItem = onConfirmQuote.breakup.find((item: any) => item['@ondc/org/title_type'] === 'offer') + // console.log('offerItem', offerItem) + // let offerApplied + // if (offerItem.length > 0) { + // let offerItemIds; + // offerItem.forEach((offer: any,index:number) => { + // offerApplied = providerOffers.find((item: any) => item.id === offer["@ondc/org/item_id"]) + // if (offerApplied) { + + // offerItemIds = offerApplied?.item_ids || [] + // const matchingItems = offerItemIds.find((id: string) => updatedItemID.includes(id)) || [] + // const match = onConfirmOfferItem.find((id:any)=>id === offer["@ondc/org/item_id"]) + // console.log('matchingItems', JSON.stringify(matchingItems)) + + // if (matchingItems.length === 0 || !matchingItems) { + // onupdtObj[`offer_item[${index}]`] = + // `Offer with id '${offer["@ondc/org/item_id"]}' is not applicable for any of the ordered item(s). \nApplicable items in offer: [${offerItemIds.join(', ')}], \nItems in order: [${offerItemIds.join(', ')}].` + // return + // } + // console.log('offer applied', JSON.stringify(offerApplied)) + // } + // }) + // } + const offerItem = on_update.quote.breakup.filter((item: any) => item['@ondc/org/title_type'] === 'offer') + const deliveryCharges = Math.abs(parseFloat(on_update.quote.breakup.find((item: any) => item["@ondc/org/title_type"] === "delivery")?.price?.value)) || 0; + const providerOffers: any = getValue(`${ApiSequence.ON_SEARCH}_offers`) + let offerApplied: any + console.log('applied ', offerApplied) + + const totalWithoutOffers = on_update?.quote?.breakup.reduce((sum: any, item: any) => { + if (item['@ondc/org/title_type'] !== 'offer') { + const value = parseFloat(item.price?.value || '0') + return sum + value + } + return sum + }, 0) + console.log("totalWithoutOffers", totalWithoutOffers); + + + const deliveryFulfillment = on_update.fulfillments.find((f: any) => f.type === "Delivery"); + console.log('deliveryFulfillment', deliveryFulfillment); + + const orderItemIds = on_update?.items + ?.filter((item: any) => item.fulfillment_id === deliveryFulfillment?.id) + ?.map((item: any) => ({ + id: item.id, + fulfillment_id: item.fulfillment_id, + })) || []; + const items: any = orderItemIds + .map((id: any) => { + const item = on_update?.quote?.breakup.find((entry: any) => entry['@ondc/org/item_id'] === id) + return item ? { id, price: item.price.value, quantity: item['@ondc/org/item_quantity']?.count } : null + }) + .filter((item: any) => item !== null) + console.log('itemPrices of found items in breakup', JSON.stringify(items)) + + const priceSums = items.reduce((acc: Record, item: { id: string; price: string }) => { + const { id, price } = item + acc[id] = (acc[id] || 0) + parseFloat(price) + return acc + }, {}) + console.log("priceSums", priceSums); + let offerPriceValue: number = 0 + let offerItemQuantity: number = 0 + console.log("offerItemQuantity", offerItemQuantity); + + if (offerItem.length > 0) { + offerItem.forEach((offer: any, i: number) => { + offerApplied = providerOffers.find((item: any) => item.id === offer['@ondc/org/item_id']) + const offerId = offer['@ondc/org/item_id'] + offerPriceValue = Math.abs(parseFloat(offer?.price?.value)) + const offerType = offer?.item?.tags + ?.find((tag: any) => tag.code === 'offer') + ?.list?.find((entry: any) => entry.code === 'type')?.value + if (offerApplied) { + console.log("offerId", offerId); + + const offerItemIds = offerApplied?.item_ids || [] + + const orderItemIdList = orderItemIds.map((item: any) => item.id) // extract just the IDs + + const matchingItems = offerItemIds.filter((id: string) => orderItemIdList.includes(id)) || [] + console.log("matchingItems", JSON.stringify(matchingItems)); const qualifierTag: any = offerApplied?.tags?.find((tag: any) => { return tag?.code === 'qualifier' }) @@ -1564,19 +1579,19 @@ export const checkOnUpdate = ( const minValue = parseFloat(qualifierList.find((l: any) => l.code === 'min_value')?.value) || 0 console.log('min_value', minValue) const itemsOnSearch: any = getValue(`onSearchItems`) - console.log('itemsOnSearch', JSON.stringify(itemsOnSearch)) - if (offerType === 'discount') { - if (minValue > 0 && minValue !== null) { - const offerApplicable: boolean = totalWithoutOffers >= minValue - console.log('qualifies', offerApplicable, minValue) - - if (minValue > 0 && totalWithoutOffers < minValue) { - onupdtObj['offerNA'] = - `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}`; - } - else{ - const benefitList = benefitTag?.list || [] - + console.log('itemsOnSearch', JSON.stringify(itemsOnSearch)) + if (offerType === 'discount') { + if (minValue > 0 && minValue !== null) { + const offerApplicable: boolean = totalWithoutOffers >= minValue + console.log('qualifies', offerApplicable, minValue) + + if (minValue > 0 && totalWithoutOffers < minValue) { + onupdtObj['offerNA'] = + `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}`; + } + else { + const benefitList = benefitTag?.list || [] + const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value const value_cap = Math.abs( parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), @@ -1586,17 +1601,17 @@ export const checkOnUpdate = ( ) const quotedPrice = parseFloat(on_update.quote.price.value || '0') let qualifies = false - + if (valueType === 'percent') { if (value_cap === 0) { onupdtObj['priceErr'] = `Offer benefit value_cap cannot be 0 for offer ID: ${offerId}` } else { const percentageDiscount = (benefitValue / 100) * totalWithoutOffers const expectedDiscount = Math.min(percentageDiscount, value_cap) - + if (Math.abs(offerPriceValue) > expectedDiscount) { onupdtObj.priceMismatch = `Discount over-applied. Maximum allowed: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` - } + } // else if (Math.abs(offerPriceValue) < expectedDiscount) { // onupdtObj.priceMismatch = `Discount under-applied. Expected: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` // } @@ -1605,576 +1620,599 @@ export const checkOnUpdate = ( if (offerPriceValue > benefitValue) { onupdtObj['priceErr'] = `Discount exceeds configured benefit. Found: -${offerPriceValue}, Allowed: -${benefitValue}` } - + const quoteAfterBenefit = totalWithoutOffers - benefitValue qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 - + if (!qualifies) { onupdtObj['priceErr'] = `Quoted price mismatch. Expected after ₹${benefitValue} off on ₹${totalWithoutOffers}: ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}` } } - } - } + } - if (offerType === 'freebie') { - - if (minValue > 0 && minValue !== null) { - console.log('benefit lsit', benefitList) - const qualifies: boolean = totalWithoutOffers >= minValue - - console.log('qualifies', qualifies, minValue) - - if (!qualifies) { - onupdtObj['offerNa'] = - `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` - } - const benefitItemId = benefitList.find((entry: any) => entry.code === 'item_id')?.value || '' - const benefitItemCount = parseInt( - benefitList.find((entry: any) => entry.code === 'item_count')?.value || '0', - ) - const itemTags = offer?.item?.tags || [] - - const offerTag = itemTags.find((tag: any) => tag.code === 'offer') - if(!offerTag){ - onupdtObj.invalidTags = `tags are required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId}` - } - - const offerItemId = offerTag?.list?.find((entry: any) => entry.code === 'item_id')?.value || '' - console.log("offerItemId",offerItemId); - - const offerItemCount = parseInt( - offerTag?.list?.find((entry: any) => entry.code === 'item_count')?.value || '0', - ) - if (!offerItemCount) { - onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} ` - } - if (offerItemId !== benefitItemId) { - onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` - } - if (benefitItemCount !== offerItemCount) { - onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_id (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` - } - if (!offerItemId) { - onupdtObj.invalidItems = `item_id is required in on_select /quote with @ondc/org/title_type:${offerType} with offer_id:${offerId}` - } - - // const offerPrice = Math.abs(parseFloat(element?.price?.value || '0')) - - const itemIds = offerItemId.split(',').map((id: string) => id.trim()) - - const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) - console.log("matchedItems",matchedItems); - if (matchedItems.length === 0) { - onupdtObj[`offer_item[${i}]`] = - `Item(s) with ID(s) ${itemIds.join(', ')} not found in catalog for offer ID: ${offerId}` - return - } - - - const priceMismatchItems: string[] = [] - let totalExpectedOfferValue:number = 0 - console.log(totalExpectedOfferValue) - let allItemsEligible = true - - matchedItems?.forEach((item: any) => { - const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) - const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) - - // Calculate the expected total price for the item - const expectedItemTotal = itemPrice * offerItemCount - totalExpectedOfferValue += expectedItemTotal - - // Validate stock availability - if (availableCount < offerItemCount) { - onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` - allItemsEligible = false - } - - // Validate price consistency - // const quotedPrice = Math.abs(parseFloat(element?.price?.value || '0')) - // if (expectedItemTotal !== quotedPrice) { - // priceMismatchItems.push( - // `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, - // ) - // allItemsEligible = false - // } - }) - - if (priceMismatchItems.length > 0) { - onupdtObj.priceMismatch1 = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` - } - - if (!allItemsEligible) { - const missingOrOutOfStock = itemIds.filter((id: string) => { - const matchedItem = matchedItems.find((item: any) => item.id === id) - if (!matchedItem) return true - const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) - return availableCount < offerItemCount - }) - - if (missingOrOutOfStock.length > 0) { - onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` - } - } + } + if (offerType === 'freebie') { + + if (minValue > 0 && minValue !== null) { + console.log('benefit lsit', benefitList) + const qualifies: boolean = totalWithoutOffers >= minValue + + console.log('qualifies', qualifies, minValue) + + if (!qualifies) { + onupdtObj['offerNa'] = + `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` } - else { - console.log('benefit lsit', benefitList) - const benefitItemId = benefitList.find((entry: any) => entry.code === 'item_id')?.value || '' - const benefitItemCount = parseInt( - benefitList.find((entry: any) => entry.code === 'item_count')?.value || '0', - ) - const itemTags = offer?.item?.tags || [] - - const offerTag = itemTags.find((tag: any) => tag.code === 'offer') - if(!offerTag){ - onupdtObj.invalidTags = `tags are required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId}` - } - - const offerItemId = offerTag?.list?.find((entry: any) => entry.code === 'item_id')?.value || '' - const offerItemCount = parseInt( - offerTag?.list?.find((entry: any) => entry.code === 'item_count')?.value || '0', - ) - if (!offerItemCount) { - onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} ` - } - if (offerItemId !== benefitItemId) { - onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` - } - if (benefitItemCount !== offerItemCount) { - onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_id (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` - } - if (!offerItemId) { - onupdtObj.invalidItems = `item_id is required in on_select /quote with @ondc/org/title_type:${offerType} with offer_id:${offerId}` - } - - // const offerPrice = Math.abs(parseFloat(element?.price?.value || '0')) - - const itemIds = offerItemId.split(',').map((id: string) => id.trim()) - // let totalExpectedOfferValue = 0 - const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) - - const priceMismatchItems: string[] = [] - let totalExpectedOfferValue = 0 - console.log(totalExpectedOfferValue) - let allItemsEligible = true - - matchedItems.forEach((item: any) => { - const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) - const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) - - // Calculate the expected total price for the item - const expectedItemTotal = itemPrice * offerItemCount - totalExpectedOfferValue += expectedItemTotal - - // Validate stock availability - if (availableCount < offerItemCount) { - onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` - allItemsEligible = false - } - - // Validate price consistency - const quotedPrice = Math.abs(parseFloat(offer?.price?.value || '0')) - if (expectedItemTotal !== quotedPrice) { - priceMismatchItems.push( - `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, - ) - allItemsEligible = false - } - }) - - // Report any price mismatches - if (priceMismatchItems.length > 0) { - onupdtObj.priceMismatch = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` - } - - // If not all items are eligible, identify missing or out-of-stock items - if (!allItemsEligible) { - const missingOrOutOfStock = itemIds.filter((id: string) => { - const matchedItem = matchedItems.find((item: any) => item.id === id) - if (!matchedItem) return true - const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) - return availableCount < offerItemCount - }) - - if (missingOrOutOfStock.length > 0) { - onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` - } - } + const benefitItemId = benefitList.find((entry: any) => entry.code === 'item_id')?.value || '' + const benefitItemCount = parseInt( + benefitList.find((entry: any) => entry.code === 'item_count')?.value || '0', + ) + const itemTags = offer?.item?.tags || [] + + const offerTag = itemTags.find((tag: any) => tag.code === 'offer') + if (!offerTag) { + onupdtObj.invalidTags = `tags are required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId}` } - } - if (offerType === 'buyXgetY') { - const offerItemsWithQuantity = items - .filter((item: any) => matchingItems.includes(item.id)) - .map((item: any) => ({ - id: item.id, - quantity: item.quantity, - })) - console.log('offerItemQuantity', offerItemsWithQuantity) - const offerMinItemCount = - parseFloat(qualifierList.find((l: any) => l.code === 'item_count')?.value) || 0 - if (!offerMinItemCount || offerMinItemCount === 0) { - onupdtObj.invalidItems = `Minimum Item Count required in catalog /offers/tags/qualifier/list/code:item_count or minimum item_count cannot be 0 for offer with id :${offerId}` + + const offerItemId = offerTag?.list?.find((entry: any) => entry.code === 'item_id')?.value || '' + console.log("offerItemId", offerItemId); + + const offerItemCount = parseInt( + offerTag?.list?.find((entry: any) => entry.code === 'item_count')?.value || '0', + ) + if (!offerItemCount) { + onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} ` } - let isOfferEligible: boolean - offerItemsWithQuantity.forEach((item: any) => { - - if (offerMinItemCount) { - isOfferEligible = item.quantity >= offerMinItemCount - } - if (!isOfferEligible) { - onupdtObj.invalidItems = `Offer with ${offerId} is not applicale as item with id: ${item?.id} and quantity: ${item.quantity} does not match with offer item_count ${offerMinItemCount}` + if (offerItemId !== benefitItemId) { + onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` + } + if (benefitItemCount !== offerItemCount) { + onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_id (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` + } + if (!offerItemId) { + onupdtObj.invalidItems = `item_id is required in on_select /quote with @ondc/org/title_type:${offerType} with offer_id:${offerId}` + } + + // const offerPrice = Math.abs(parseFloat(element?.price?.value || '0')) + + const itemIds = offerItemId.split(',').map((id: string) => id.trim()) + + const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) + console.log("matchedItems", matchedItems); + if (matchedItems.length === 0) { + onupdtObj[`offer_item[${i}]`] = + `Item(s) with ID(s) ${itemIds.join(', ')} not found in catalog for offer ID: ${offerId}` + return + } + + + const priceMismatchItems: string[] = [] + let totalExpectedOfferValue: number = 0 + console.log(totalExpectedOfferValue) + let allItemsEligible = true + + matchedItems?.forEach((item: any) => { + const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) + const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) + + // Calculate the expected total price for the item + const expectedItemTotal = itemPrice * offerItemCount + totalExpectedOfferValue += expectedItemTotal + + // Validate stock availability + if (availableCount < offerItemCount) { + onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` + allItemsEligible = false } + + // Validate price consistency + // const quotedPrice = Math.abs(parseFloat(element?.price?.value || '0')) + // if (expectedItemTotal !== quotedPrice) { + // priceMismatchItems.push( + // `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, + // ) + // allItemsEligible = false + // } }) - if (minValue > 0 && minValue !== null) { - const qualifies: boolean = minValue >= priceSums - console.log('qualifies', qualifies, minValue) - - if (!qualifies) { - onupdtObj['offerNa'] = - `Offer with id: ${offerId} not applicable as required ${minValue} min_value for order is not satisfied` + + if (priceMismatchItems.length > 0) { + onupdtObj.priceMismatch1 = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` + } + + if (!allItemsEligible) { + const missingOrOutOfStock = itemIds.filter((id: string) => { + const matchedItem = matchedItems.find((item: any) => item.id === id) + if (!matchedItem) return true + const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) + return availableCount < offerItemCount + }) + + if (missingOrOutOfStock.length > 0) { + onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` } } + } + else { + console.log('benefit lsit', benefitList) const benefitItemId = benefitList.find((entry: any) => entry.code === 'item_id')?.value || '' const benefitItemCount = parseInt( benefitList.find((entry: any) => entry.code === 'item_count')?.value || '0', ) - const benefitItemValue = parseInt( - benefitList.find((entry: any) => entry.code === 'item_value')?.value || '0', - ) const itemTags = offer?.item?.tags || [] - + const offerTag = itemTags.find((tag: any) => tag.code === 'offer') - const offerItemValue = offerTag?.list?.find((entry: any) => entry.code === 'item_value')?.value || '' - const quotedPrice = (parseFloat(offer?.price?.value || '0')) - if(quotedPrice<0){ - onupdtObj.invalidPrice = `Price for Item with id: ${offerId} cannot be negative.` - } - if(benefitItemValue<0){ - onupdtObj.invalidPrice = `Benefit Value for tag benefit and item with id: ${offerId} cannot be negative.` - } - console.log("quotedPrice",quotedPrice,benefitItemValue); - - if(quotedPrice !== offerItemValue){ - onupdtObj.priceMismatch = `value mismatch benefit_value for tag "item_value" in on_select ${offerItemValue} does not match with /quote/item with itemId: ${offerId}` - } - if(offerItemValue !== benefitItemValue){ - onupdtObj.benefitValueMismatch = `value mismatch benefit_value ${benefitItemValue} does not match with catalog item_value ${offerItemValue} /quote/item with itemId: ${offerId}` - } - if(!offerTag){ + if (!offerTag) { onupdtObj.invalidTags = `tags are required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId}` } - + const offerItemId = offerTag?.list?.find((entry: any) => entry.code === 'item_id')?.value || '' const offerItemCount = parseInt( offerTag?.list?.find((entry: any) => entry.code === 'item_count')?.value || '0', ) if (!offerItemCount) { - onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId} ` + onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} ` } if (offerItemId !== benefitItemId) { - onupdtObj.invalidBenefitItem = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` + onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` } if (benefitItemCount !== offerItemCount) { - onupdtObj.invalidItemCount = `Mismatch: item_count used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_count (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` + onupdtObj.invalidItems = `Mismatch: item_id used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_id (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` } - if (offerItemId) { - const itemIds = offerItemId.split(',').map((id: string) => id.trim()) - // let totalExpectedOfferValue = 0 - const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) - - const priceMismatchItems: string[] = [] - let totalExpectedOfferValue = 0 - console.log(totalExpectedOfferValue) - let allItemsEligible = true - - matchedItems.forEach((item: any) => { - const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) - const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) - - // Calculate the expected total price for the item - const expectedItemTotal = itemPrice * offerItemCount - totalExpectedOfferValue += expectedItemTotal - - // Validate stock availability - if (availableCount < offerItemCount) { - onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` - allItemsEligible = false - } - - // Validate price consistency - if (expectedItemTotal !== quotedPrice) { - priceMismatchItems.push( - `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, - ) - allItemsEligible = false - } - }) - - // Report any price mismatches - if (priceMismatchItems.length > 0) { - onupdtObj.priceMismatch = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` + if (!offerItemId) { + onupdtObj.invalidItems = `item_id is required in on_select /quote with @ondc/org/title_type:${offerType} with offer_id:${offerId}` + } + + // const offerPrice = Math.abs(parseFloat(element?.price?.value || '0')) + + const itemIds = offerItemId.split(',').map((id: string) => id.trim()) + // let totalExpectedOfferValue = 0 + const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) + + const priceMismatchItems: string[] = [] + let totalExpectedOfferValue = 0 + console.log(totalExpectedOfferValue) + let allItemsEligible = true + + matchedItems.forEach((item: any) => { + const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) + const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) + + // Calculate the expected total price for the item + const expectedItemTotal = itemPrice * offerItemCount + totalExpectedOfferValue += expectedItemTotal + + // Validate stock availability + if (availableCount < offerItemCount) { + onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` + allItemsEligible = false } - - // If not all items are eligible, identify missing or out-of-stock items - if (!allItemsEligible) { - const missingOrOutOfStock = itemIds.filter((id: string) => { - const matchedItem = matchedItems.find((item: any) => item.id === id) - if (!matchedItem) return true - const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) - return availableCount < offerItemCount - }) - - if (missingOrOutOfStock.length > 0) { - onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` - } + + // Validate price consistency + const quotedPrice = Math.abs(parseFloat(offer?.price?.value || '0')) + if (expectedItemTotal !== quotedPrice) { + priceMismatchItems.push( + `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, + ) + allItemsEligible = false + } + }) + + // Report any price mismatches + if (priceMismatchItems.length > 0) { + onupdtObj.priceMismatch = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` + } + + // If not all items are eligible, identify missing or out-of-stock items + if (!allItemsEligible) { + const missingOrOutOfStock = itemIds.filter((id: string) => { + const matchedItem = matchedItems.find((item: any) => item.id === id) + if (!matchedItem) return true + const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) + return availableCount < offerItemCount + }) + + if (missingOrOutOfStock.length > 0) { + onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` } } } - if (offerType === 'delivery') { - if (deliveryCharges > 0 || deliveryCharges !== null) { - const offerApplicable: boolean = totalWithoutOffers >= minValue - console.log('qualifies', offerApplicable, minValue) - - if (minValue > 0 && totalWithoutOffers < minValue) { - onupdtObj['offerNA'] = - `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` - } else { - const benefitList = benefitTag?.list || [] + } + if (offerType === 'buyXgetY') { + const offerItemsWithQuantity = items + .filter((item: any) => matchingItems.includes(item.id)) + .map((item: any) => ({ + id: item.id, + quantity: item.quantity, + })) + console.log('offerItemQuantity', offerItemsWithQuantity) + const offerMinItemCount = + parseFloat(qualifierList.find((l: any) => l.code === 'item_count')?.value) || 0 + if (!offerMinItemCount || offerMinItemCount === 0) { + onupdtObj.invalidItems = `Minimum Item Count required in catalog /offers/tags/qualifier/list/code:item_count or minimum item_count cannot be 0 for offer with id :${offerId}` + } + let isOfferEligible: boolean + offerItemsWithQuantity.forEach((item: any) => { - const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value - const value_cap = Math.abs( - parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), - ) - const benefitValue = Math.abs( - parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), + if (offerMinItemCount) { + isOfferEligible = item.quantity >= offerMinItemCount + } + if (!isOfferEligible) { + onupdtObj.invalidItems = `Offer with ${offerId} is not applicale as item with id: ${item?.id} and quantity: ${item.quantity} does not match with offer item_count ${offerMinItemCount}` + } + }) + if (minValue > 0 && minValue !== null) { + const qualifies: boolean = minValue >= priceSums + console.log('qualifies', qualifies, minValue) + + if (!qualifies) { + onupdtObj['offerNa'] = + `Offer with id: ${offerId} not applicable as required ${minValue} min_value for order is not satisfied` + } + } + const benefitItemId = benefitList.find((entry: any) => entry.code === 'item_id')?.value || '' + const benefitItemCount = parseInt( + benefitList.find((entry: any) => entry.code === 'item_count')?.value || '0', + ) + const benefitItemValue = parseInt( + benefitList.find((entry: any) => entry.code === 'item_value')?.value || '0', + ) + const itemTags = offer?.item?.tags || [] + + const offerTag = itemTags.find((tag: any) => tag.code === 'offer') + const offerItemValue = offerTag?.list?.find((entry: any) => entry.code === 'item_value')?.value || '' + const quotedPrice = (parseFloat(offer?.price?.value || '0')) + if (quotedPrice < 0) { + onupdtObj.invalidPrice = `Price for Item with id: ${offerId} cannot be negative.` + } + if (benefitItemValue < 0) { + onupdtObj.invalidPrice = `Benefit Value for tag benefit and item with id: ${offerId} cannot be negative.` + } + console.log("quotedPrice", quotedPrice, benefitItemValue); + + if (quotedPrice !== offerItemValue) { + onupdtObj.priceMismatch = `value mismatch benefit_value for tag "item_value" in on_select ${offerItemValue} does not match with /quote/item with itemId: ${offerId}` + } + if (offerItemValue !== benefitItemValue) { + onupdtObj.benefitValueMismatch = `value mismatch benefit_value ${benefitItemValue} does not match with catalog item_value ${offerItemValue} /quote/item with itemId: ${offerId}` + } + if (!offerTag) { + onupdtObj.invalidTags = `tags are required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId}` + } + + const offerItemId = offerTag?.list?.find((entry: any) => entry.code === 'item_id')?.value || '' + const offerItemCount = parseInt( + offerTag?.list?.find((entry: any) => entry.code === 'item_count')?.value || '0', + ) + if (!offerItemCount) { + onupdtObj.invalidItems = `item_count is required in on_select /quote with @ondc/org/title_type:${offerType} and offerId:${offerId} ` + } + if (offerItemId !== benefitItemId) { + onupdtObj.invalidBenefitItem = `Mismatch: item_id used in on_select quote.breakup (${offerItemId}) doesn't match with offer benefit item_id (${benefitItemId}) in on_search catalog for offer ID: ${offerId}` + } + if (benefitItemCount !== offerItemCount) { + onupdtObj.invalidItemCount = `Mismatch: item_count used in on_select quote.breakup (${offerItemCount}) quantity doesn't match with offer benefit item_count (${benefitItemCount}) in on_search catalog for offer ID: ${offerId}` + } + if (offerItemId) { + const itemIds = offerItemId.split(',').map((id: string) => id.trim()) + // let totalExpectedOfferValue = 0 + const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) + + const priceMismatchItems: string[] = [] + let totalExpectedOfferValue = 0 + console.log(totalExpectedOfferValue) + let allItemsEligible = true + + matchedItems.forEach((item: any) => { + const itemPrice = Math.abs(parseFloat(item?.price?.value || '0')) + const availableCount = parseInt(item?.quantity?.available?.count || '0', 10) + + // Calculate the expected total price for the item + const expectedItemTotal = itemPrice * offerItemCount + totalExpectedOfferValue += expectedItemTotal + + // Validate stock availability + if (availableCount < offerItemCount) { + onupdtObj.invalidItems = `Item ID: ${item.id} does not have sufficient stock. Required: ${offerItemCount}, Available: ${availableCount}` + allItemsEligible = false + } + + // Validate price consistency + if (expectedItemTotal !== quotedPrice) { + priceMismatchItems.push( + `ID: ${item.id} (Expected Total: ₹${expectedItemTotal}, Quoted: ₹${quotedPrice})`, ) - const quotedPrice = parseFloat(on_update.quote.price.value || '0') - let qualifies = false - - if (valueType === 'percent') { - if (value_cap === 0) { - onupdtObj['priceErr'] = `Offer benefit amount cannot be equal to ${value_cap}` - } else { - console.log('delivery charges', deliveryCharges, offerPriceValue) - const percentageDiscount = (benefitValue / 100) * deliveryCharges + allItemsEligible = false + } + }) + + // Report any price mismatches + if (priceMismatchItems.length > 0) { + onupdtObj.priceMismatch = `Price mismatch found for item(s): ${priceMismatchItems.join('; ')}` + } + + // If not all items are eligible, identify missing or out-of-stock items + if (!allItemsEligible) { + const missingOrOutOfStock = itemIds.filter((id: string) => { + const matchedItem = matchedItems.find((item: any) => item.id === id) + if (!matchedItem) return true + const availableCount = parseInt(matchedItem?.quantity?.available?.count || '0', 10) + return availableCount < offerItemCount + }) + + if (missingOrOutOfStock.length > 0) { + onupdtObj.invalidItems = `Item(s) with ID(s) ${missingOrOutOfStock.join(', ')} not found in catalog or do not have enough stock for offer ID: ${offerId}` + } + } + } + } + if (offerType === 'delivery') { + if (deliveryCharges > 0 || deliveryCharges !== null) { + const offerApplicable: boolean = totalWithoutOffers >= minValue + console.log('qualifies', offerApplicable, minValue) + + if (minValue > 0 && totalWithoutOffers < minValue) { + onupdtObj['offerNA'] = + `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` + } else { + const benefitList = benefitTag?.list || [] + + const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value + const value_cap = Math.abs( + parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), + ) + const benefitValue = Math.abs( + parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), + ) + const quotedPrice = parseFloat(on_update.quote.price.value || '0') + let qualifies = false + + if (valueType === 'percent') { + if (value_cap === 0) { + onupdtObj['priceErr'] = `Offer benefit amount cannot be equal to ${value_cap}` + } else { + console.log('delivery charges', deliveryCharges, offerPriceValue) + const percentageDiscount = (benefitValue / 100) * deliveryCharges const expectedDiscount = Math.min(percentageDiscount, value_cap) - + if (Math.abs(offerPriceValue) > expectedDiscount) { onupdtObj.priceMismatch = `Discount over-applied. Maximum allowed: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` - } - if (expectedDiscount > deliveryCharges) { - onupdtObj.priceMismatch = `Discount exceeds delivery charge. Discount: ₹${expectedDiscount.toFixed(2)}, Delivery Charge: ₹${deliveryCharges.toFixed(2)}` - } } - } else { - if (offerPriceValue > benefitValue) { - onupdtObj['priceErr'] = - `Discount mismatch: Delivery Offer discount cannot exceed -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` + if (expectedDiscount > deliveryCharges) { + onupdtObj.priceMismatch = `Discount exceeds delivery charge. Discount: ₹${expectedDiscount.toFixed(2)}, Delivery Charge: ₹${deliveryCharges.toFixed(2)}` } + } + } else { + if (offerPriceValue > benefitValue) { + onupdtObj['priceErr'] = + `Discount mismatch: Delivery Offer discount cannot exceed -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` + } - const quoteAfterBenefit = totalWithoutOffers - benefitValue + const quoteAfterBenefit = totalWithoutOffers - benefitValue - qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 + qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 - if (!qualifies) { - onupdtObj['priceErr'] = - `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` - } + if (!qualifies) { + onupdtObj['priceErr'] = + `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` } } - } else { - onupdtObj.invalidOfferType = `item with id: ${offerId} in quote.breakup[${i}] does not exist in items[]. Hence offer cannot be applied for this order.` } + } else { + onupdtObj.invalidOfferType = `item with id: ${offerId} in quote.breakup[${i}] does not exist in items[]. Hence offer cannot be applied for this order.` } - if (offerType === 'combo') { - const qualifierItems = qualifierList.find((item:any)=>item.code === "item_id").value - console.log("qualifierItems",qualifierItems); - const itemIds = qualifierItems.split(',').map((id: string) => id.trim()) - if(!itemIds){ - onupdtObj.invalidItems = `item_id is required in catalog for code:qualifier /offers/tags/list/value @ondc/org/title_type:${offerType} with offer_id:${offerId}` - } - - const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) - if (matchedItems.length !== itemIds.length) { - onupdtObj.invalidItems = `One or more item IDs are missing in the search results` - } - - if (minValue > 0 && minValue !== null) { - const qualifies: boolean = totalWithoutOffers >= minValue - - console.log('qualifies', qualifies, minValue) - - if (!qualifies) { - onupdtObj['priceErr'] = - `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` - } + } + if (offerType === 'combo') { + const qualifierItems = qualifierList.find((item: any) => item.code === "item_id").value + console.log("qualifierItems", qualifierItems); + const itemIds = qualifierItems.split(',').map((id: string) => id.trim()) + if (!itemIds) { + onupdtObj.invalidItems = `item_id is required in catalog for code:qualifier /offers/tags/list/value @ondc/org/title_type:${offerType} with offer_id:${offerId}` + } + + const matchedItems = itemsOnSearch[0].filter((item: any) => itemIds.includes(item.id)) + if (matchedItems.length !== itemIds.length) { + onupdtObj.invalidItems = `One or more item IDs are missing in the search results` + } + + if (minValue > 0 && minValue !== null) { + const qualifies: boolean = totalWithoutOffers >= minValue + + console.log('qualifies', qualifies, minValue) + + if (!qualifies) { + onupdtObj['priceErr'] = + `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` } - const benefitList = benefitTag?.list || [] - - const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value - const value_cap = Math.abs( - parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), - ) - const benefitValue = Math.abs( - parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), - ) - const quotedPrice = parseFloat(on_update.quote.price.value || '0') - let qualifies = false - - if (valueType === 'percent') { - if (value_cap === 0) { - onupdtObj['priceErr'] = `Offer benefit value_cap cannot be equal to ${value_cap}` - } else { - console.log('delivery charges', offerPriceValue) - let expectedDiscount = 0 - expectedDiscount = (benefitValue / 100) * totalWithoutOffers - if (expectedDiscount > value_cap) { - onupdtObj.invalidOfferBenefit = `offer discount ${expectedDiscount} exceeds value_cap ${value_cap}` - expectedDiscount = value_cap - } - if (offerPriceValue !== expectedDiscount) { - onupdtObj.priceMismatch = `Discount value mismatch. Expected: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` - } - } + } + const benefitList = benefitTag?.list || [] + + const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value + const value_cap = Math.abs( + parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), + ) + const benefitValue = Math.abs( + parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), + ) + const quotedPrice = parseFloat(on_update.quote.price.value || '0') + let qualifies = false + + if (valueType === 'percent') { + if (value_cap === 0) { + onupdtObj['priceErr'] = `Offer benefit value_cap cannot be equal to ${value_cap}` } else { - if (benefitValue !== offerPriceValue) { - onupdtObj['priceErr'] = - `Discount mismatch: Expected discount is -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` + console.log('delivery charges', offerPriceValue) + let expectedDiscount = 0 + expectedDiscount = (benefitValue / 100) * totalWithoutOffers + if (expectedDiscount > value_cap) { + onupdtObj.invalidOfferBenefit = `offer discount ${expectedDiscount} exceeds value_cap ${value_cap}` + expectedDiscount = value_cap } - - const quoteAfterBenefit = totalWithoutOffers - benefitValue - - qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 - - if (!qualifies) { - onupdtObj['priceErr'] = - `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` + if (offerPriceValue !== expectedDiscount) { + onupdtObj.priceMismatch = `Discount value mismatch. Expected: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` } } - } - if (offerType === 'slab') { - const offerItemsWithQuantity = items - .filter((item: any) => matchingItems.includes(item.id)) - .map((item: any) => ({ - id: item.id, - quantity: item.quantity, - })) - console.log('offerItemQuantity', offerItemsWithQuantity) - - const offerMinItemCount = - parseFloat(qualifierList.find((l: any) => l.code === 'item_count')?.value) || 0 - if (!offerMinItemCount || offerMinItemCount === 0) { - onupdtObj.invalidItems = `Minimum Item Count required in catalog /offers/tags/qualifier/list/code:item_count or minimum item_count cannot be 0 for offer with id :${offerId}` + } else { + if (benefitValue !== offerPriceValue) { + onupdtObj['priceErr'] = + `Discount mismatch: Expected discount is -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` } - const itemCountUpperQualifier = qualifierList.find((l: any) => l.code === 'item_count_upper') - console.log('itemCountUpperQualifier', itemCountUpperQualifier) - - if (!itemCountUpperQualifier) { - onupdtObj.invalidItems = `The "item_count_upper" qualifier is required but was not provided.` + + const quoteAfterBenefit = totalWithoutOffers - benefitValue + + qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 + + if (!qualifies) { + onupdtObj['priceErr'] = + `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` } - const itemCountUpperRaw = itemCountUpperQualifier?.value - let itemCountUpper: any - - if (itemCountUpperRaw === undefined || itemCountUpperRaw.trim() === '') { - // No upper limit specified + } + } + if (offerType === 'slab') { + const offerItemsWithQuantity = items + .filter((item: any) => matchingItems.includes(item.id)) + .map((item: any) => ({ + id: item.id, + quantity: item.quantity, + })) + console.log('offerItemQuantity', offerItemsWithQuantity) + + const offerMinItemCount = + parseFloat(qualifierList.find((l: any) => l.code === 'item_count')?.value) || 0 + if (!offerMinItemCount || offerMinItemCount === 0) { + onupdtObj.invalidItems = `Minimum Item Count required in catalog /offers/tags/qualifier/list/code:item_count or minimum item_count cannot be 0 for offer with id :${offerId}` + } + const itemCountUpperQualifier = qualifierList.find((l: any) => l.code === 'item_count_upper') + console.log('itemCountUpperQualifier', itemCountUpperQualifier) + + if (!itemCountUpperQualifier) { + onupdtObj.invalidItems = `The "item_count_upper" qualifier is required but was not provided.` + } + const itemCountUpperRaw = itemCountUpperQualifier?.value + let itemCountUpper: any + + if (itemCountUpperRaw === undefined || itemCountUpperRaw.trim() === '') { + // No upper limit specified + itemCountUpper = null + } else { + itemCountUpper = parseFloat(itemCountUpperRaw) + if (isNaN(itemCountUpper)) { + // Handle invalid number format if necessary itemCountUpper = null - } else { - itemCountUpper = parseFloat(itemCountUpperRaw) - if (isNaN(itemCountUpper)) { - // Handle invalid number format if necessary - itemCountUpper = null - } } - - if (itemCountUpper !== null && itemCountUpper < offerMinItemCount) { - onupdtObj.invalidItems = `Invalid configuration: item_count_upper (${itemCountUpper}) cannot be less than item_count (${offerMinItemCount}).` + } + + if (itemCountUpper !== null && itemCountUpper < offerMinItemCount) { + onupdtObj.invalidItems = `Invalid configuration: item_count_upper (${itemCountUpper}) cannot be less than item_count (${offerMinItemCount}).` + } + let isOfferEligible: boolean + offerItemsWithQuantity.forEach((item: any) => { + if (!itemCountUpper) { + isOfferEligible = item.quantity >= offerMinItemCount } - let isOfferEligible: boolean - offerItemsWithQuantity.forEach((item: any) => { - if (!itemCountUpper) { - isOfferEligible = item.quantity >= offerMinItemCount - } - if (itemCountUpper) { - isOfferEligible = item.quantity >= offerMinItemCount && item?.quantity <= itemCountUpper - } - if (!isOfferEligible) { - onupdtObj.invalidItems = `Offer with ${offerId} is not applicale as item with id: ${item?.id} and quantity: ${item.quantity} does not match with offer item_count ${offerMinItemCount} and item_count_upper ${itemCountUpper}` - } - }) - if (minValue > 0 && minValue !== null) { - const qualifies: boolean = totalWithoutOffers >= minValue - - console.log('qualifies', qualifies, minValue) - - if (!qualifies) { - onupdtObj['priceErr'] = - `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` - } + if (itemCountUpper) { + isOfferEligible = item.quantity >= offerMinItemCount && item?.quantity <= itemCountUpper } - const benefitList = benefitTag?.list || [] - - const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value - const value_cap = Math.abs( - parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), - ) - const benefitValue = Math.abs( - parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), - ) - const quotedPrice = parseFloat(on_update.quote.price.value || '0') - let qualifies = false - - if (valueType === 'percent') { - if (value_cap === 0) { - onupdtObj['priceErr'] = `Offer benefit value_cap cannot be equal to ${value_cap}` - } else { - console.log('delivery charges', offerPriceValue) - let expectedDiscount = 0 - expectedDiscount = (benefitValue / 100) * totalWithoutOffers - if (expectedDiscount > value_cap) { - onupdtObj.invalidOfferBenefit = `offer discount ${expectedDiscount} exceeds value_cap ${value_cap}` - expectedDiscount = value_cap - } - // if (expectedDiscount > deliveryCharges) { - // onupdtObj.priceMismatch = `Discount exceeds delivery charge. Discount: ₹${expectedDiscount.toFixed(2)}, Delivery Charge: ₹${deliveryCharges.toFixed(2)}` - // } - if (offerPriceValue !== expectedDiscount) { - onupdtObj.priceMismatch = `Discount value mismatch. Expected: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` - } - } + if (!isOfferEligible) { + onupdtObj.invalidItems = `Offer with ${offerId} is not applicale as item with id: ${item?.id} and quantity: ${item.quantity} does not match with offer item_count ${offerMinItemCount} and item_count_upper ${itemCountUpper}` + } + }) + if (minValue > 0 && minValue !== null) { + const qualifies: boolean = totalWithoutOffers >= minValue + + console.log('qualifies', qualifies, minValue) + + if (!qualifies) { + onupdtObj['priceErr'] = + `Offer not applicable for quote with actual quote value before discount is ${totalWithoutOffers} as required min_value for order is ${minValue}` + } + } + const benefitList = benefitTag?.list || [] + + const valueType = benefitList.find((l: any) => l?.code === 'value_type')?.value + const value_cap = Math.abs( + parseFloat(benefitList.find((l: any) => l?.code === 'value_cap')?.value || '0'), + ) + const benefitValue = Math.abs( + parseFloat(benefitList.find((l: any) => l.code === 'value')?.value || '0'), + ) + const quotedPrice = parseFloat(on_update.quote.price.value || '0') + let qualifies = false + + if (valueType === 'percent') { + if (value_cap === 0) { + onupdtObj['priceErr'] = `Offer benefit value_cap cannot be equal to ${value_cap}` } else { - if (benefitValue !== offerPriceValue) { - onupdtObj['priceErr'] = - `Discount mismatch: Expected discount is -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` + console.log('delivery charges', offerPriceValue) + let expectedDiscount = 0 + expectedDiscount = (benefitValue / 100) * totalWithoutOffers + if (expectedDiscount > value_cap) { + onupdtObj.invalidOfferBenefit = `offer discount ${expectedDiscount} exceeds value_cap ${value_cap}` + expectedDiscount = value_cap } - - const quoteAfterBenefit = totalWithoutOffers - benefitValue - - qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 - - if (!qualifies) { - onupdtObj['priceErr'] = - `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` + // if (expectedDiscount > deliveryCharges) { + // onupdtObj.priceMismatch = `Discount exceeds delivery charge. Discount: ₹${expectedDiscount.toFixed(2)}, Delivery Charge: ₹${deliveryCharges.toFixed(2)}` + // } + if (offerPriceValue !== expectedDiscount) { + onupdtObj.priceMismatch = `Discount value mismatch. Expected: -${expectedDiscount.toFixed(2)}, Found: -${offerPriceValue.toFixed(2)}` } } + } else { + if (benefitValue !== offerPriceValue) { + onupdtObj['priceErr'] = + `Discount mismatch: Expected discount is -₹${benefitValue.toFixed(2)}, but found -₹${offerPriceValue.toFixed(2)}. in offer with ${offerId} in ${offerType}` + } + + const quoteAfterBenefit = totalWithoutOffers - benefitValue + + qualifies = Math.abs(quoteAfterBenefit - quotedPrice) < 0.01 + + if (!qualifies) { + onupdtObj['priceErr'] = + `Quoted price mismatch: After ₹${benefitValue} discount on ₹${totalWithoutOffers}, expected price is ₹${quoteAfterBenefit.toFixed(2)}, but got ₹${quotedPrice.toFixed(2)}.` + } } + } } - }) + }) + } } + } catch (error: any) { + console.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) } - } catch (error: any) { - console.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) + // try { + // if(flow === FLOW.FLOW00C || flow === OFFERSFLOW.FLOW0097){ + + // } + // } catch (error) { + + // } + } + // --- stateless & schemaValidation logic --- + const hasSchema = Object.keys(schemaErrors).length > 0; + const hasBusiness = Object.keys(onupdtObj).length > 0; + + if (stateless) { + if (schemaValidation === true) { + return Object.keys(schemaErrors).length ? { schemaErrors } : false; + } + if (schemaValidation === false) { + return Object.keys(onupdtObj).length ? { businessErrors: onupdtObj } : false; + } + if (!hasSchema && !hasBusiness) return false; + return { schemaErrors, businessErrors: onupdtObj }; + } + + if (schemaValidation === true) { + return Object.keys(schemaErrors).length ? { schemaErrors } : false; + } else if (schemaValidation === false) { + return Object.keys(onupdtObj).length ? { businessErrors: onupdtObj } : false; + } else { + return { schemaErrors, businessErrors: onupdtObj }; } - // try { - // if(flow === FLOW.FLOW00C || flow === OFFERSFLOW.FLOW0097){ - - // } - // } catch (error) { - - // } - return onupdtObj } catch (error: any) { logger.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) + return { [apiSeq]: `Some error occurred while checking /${apiSeq} API` } } } diff --git a/utils/Retail_.1.2.5/Update/update.ts b/utils/Retail_.1.2.5/Update/update.ts index b8f251e3..3c2d21d4 100644 --- a/utils/Retail_.1.2.5/Update/update.ts +++ b/utils/Retail_.1.2.5/Update/update.ts @@ -7,25 +7,28 @@ import { condition_id } from '../../../constants/reasonCode' import { FLOW, OFFERSFLOW } from '../../enum' import { electronicsData } from '../../../constants/electronics' -export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDetatilSet: any, flow: any) => { +export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDetatilSet: any, flow: any, schemaValidation?: boolean, stateless?: boolean) => { const updtObj: any = {} + const schemaErrors: any = {} + try { if (!data || isObjectEmpty(data)) { return { [ApiSequence.UPDATE]: 'JSON cannot be empty' } } const { message, context }: any = data - const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) - const select: any = getValue(`${ApiSequence.SELECT}`) - const allOnSearchItems: any = getValue('onSearchItems') - let onSearchItems = allOnSearchItems.flat() - let exchangeItem:any = {} + const searchContext: any = getValue(`${ApiSequence.SEARCH}_context`) || {} + const select: any = getValue(`${ApiSequence.SELECT}`) || { context: {} } + const allOnSearchItems: any = getValue('onSearchItems') || [] + let onSearchItems = Array.isArray(allOnSearchItems) ? allOnSearchItems.flat() : [] + let exchangeItem: any = {} + if (!message || !context || isObjectEmpty(message)) { return { missingFields: '/context, /message, is missing or empty' } } const update = message.order - const selectItemList: any = getValue('SelectItemList') + const selectItemList: any = getValue('SelectItemList') || [] try { logger.info(`Adding Message Id /${apiSeq}`) @@ -36,7 +39,7 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet // for update and on_update_interim if (flow === '6-b' && apiSeq == ApiSequence.UPDATE_REVERSE_QC) { setValue(`${ApiSequence.UPDATE_REVERSE_QC}_msgId`, data.context.message_id) } if (flow === '6-c' && apiSeq == ApiSequence.UPDATE_LIQUIDATED) { setValue(`${ApiSequence.UPDATE_LIQUIDATED}_msgId`, data.context.message_id) } - if(flow === '00B' && apiSeq == ApiSequence.UPDATE_REPLACEMENT){ setValue(`${ApiSequence.UPDATE_REPLACEMENT}_msgId`,data.context.message_id)} + if (flow === '00B' && apiSeq == ApiSequence.UPDATE_REPLACEMENT) { setValue(`${ApiSequence.UPDATE_REPLACEMENT}_msgId`, data.context.message_id) } } catch (error: any) { logger.error(`!!Error while checking message id for /${apiSeq}, ${error.stack}`) } @@ -131,17 +134,17 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet } else { updtObj[`returnFulfillment`] = `Return fulfillment/tags/list is missing in ${apiSeq}` } - if(flow === "00B" && replaceArr.length===0){ + if (flow === "00B" && replaceArr.length === 0) { updtObj.replaceValue = `replace' obj is required in ${apiSeq} when flow is '00b`; } - if(flow === "00B" && replaceArr.length===0){ + if (flow === "00B" && replaceArr.length === 0) { updtObj.replaceValue = `replace' obj is required in ${apiSeq} when flow is '00b`; } if (replaceArr.length > 0 && replaceArr[0]?.value) { replaceValue = replaceArr[0]?.value if (replaceValue === 'yes' || replaceValue === 'no') { - setValue('update_replace_value',replaceValue) + setValue('update_replace_value', replaceValue) logger.info(`Valid replace value: ${replaceValue} for /${apiSeq}`) } else { updtObj['returnFulfillment/code/replace'] = `Invalid replace value: ${replaceValue} in ${apiSeq}` @@ -201,18 +204,37 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet } // Validating Schema - const schemaValidation = validateSchemaRetailV2(context.domain.split(':')[1], constants.UPDATE, data) - - if (schemaValidation !== 'error') { - Object.assign(updtObj, schemaValidation) + // Validating Schema - conditional based on schemaValidation flag + const schemaValidationResult = + schemaValidation !== false + ? validateSchemaRetailV2(context.domain.split(':')[1], constants.UPDATE, data) + : 'skip' + + if (schemaValidationResult !== 'error' && schemaValidationResult !== 'skip') { + Object.assign(schemaErrors, schemaValidationResult) } + // Checking bap_id and bpp_id format const checkBap = checkBppIdOrBapId(context.bap_id) const checkBpp = checkBppIdOrBapId(context.bpp_id) if (checkBap) Object.assign(updtObj, { bap_id: 'context/bap_id should not be a url' }) if (checkBpp) Object.assign(updtObj, { bpp_id: 'context/bpp_id should not be a url' }) + if (stateless) { + const hasSchema = Object.keys(schemaErrors).length > 0 + const hasBusiness = Object.keys(updtObj).length > 0 + + if (schemaValidation === true) { + return hasSchema ? schemaErrors : false + } + if (schemaValidation === false) { + return hasBusiness ? updtObj : false + } + if (!hasSchema && !hasBusiness) return false + return { schemaErrors, businessErrors: updtObj } + } + if (!_.isEqual(data.context.domain.split(':')[1], getValue(`domain`))) { updtObj[`Domain[${data.context.action}]`] = `Domain should be same in each action` } @@ -292,7 +314,7 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet } } // Checking for return_request object in /Update - if (update.fulfillments[0].tags) { + if (update.fulfillments && update.fulfillments.length > 0 && update.fulfillments[0].tags) { try { logger.info(`Checking for return_request object in /${apiSeq}`) const updateItemSet: any = {} @@ -310,7 +332,7 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet return } let key: any = null - exchangeItem = tag.list.find((item:any)=>item.code === "exchange") + exchangeItem = tag.list.find((item: any) => item.code === "exchange") tag.list.forEach((item: any) => { if (item.code === 'item_id') { key = item.value @@ -327,8 +349,8 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet } if (item.code === 'id') { - console.log("itemFlfllmnts",itemFlfllmnts); - + console.log("itemFlfllmnts", itemFlfllmnts); + const valuesArray = Object.values((itemFlfllmnts)) if (valuesArray.includes(item.value)) { updtObj.nonUniqueReturnFulfillment = `${item.value} is not a unique fulfillment` @@ -344,7 +366,7 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet logger.error(`Invalid replace value: ${replaceValue} for /${apiSeq}`) updtObj.replaceValue = `Invalid replace value: ${replaceValue} in ${apiSeq} (valid: 'yes' or 'no')` } - setValue('update_replace_value',replaceValue) + setValue('update_replace_value', replaceValue) } @@ -365,9 +387,9 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet if (item.code === 'images') { // const images = item.value - const images:any = [] + const images: any = [] images.push(item.value) - + const allurls = images?.every((img: string) => isValidUrl(img)) if (!allurls) { logger.error( @@ -446,17 +468,17 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet } try { - if(flow === FLOW.FLOW00C || flow === OFFERSFLOW.FLOW0097){ + if (flow === FLOW.FLOW00C || flow === OFFERSFLOW.FLOW0097) { - console.log("onSearchItems",onSearchItems); - if(context.domain !== "ONDC:RET14" || context.domain !== "ONDC:RET15"){ + console.log("onSearchItems", onSearchItems); + if (context.domain !== "ONDC:RET14" || context.domain !== "ONDC:RET15") { updtObj['unsupportedType'] = `exchange is not possible for ${context.domain} as supported domains are 'ONDC:RET14','ONDC:RET15' is required for flow: ${flow}` } - let returnFulfillment = update.fulfillments.filter((fulfillment:any)=>fulfillment.type === "Return") - console.log("returnFulfillment",returnFulfillment); - - returnFulfillment.forEach((fulfillment:any)=>{ + let returnFulfillment = update.fulfillments.filter((fulfillment: any) => fulfillment.type === "Return") + console.log("returnFulfillment", returnFulfillment); + + returnFulfillment.forEach((fulfillment: any) => { fulfillment.tags?.forEach((tag: any) => { if (tag.code === 'return_request') { @@ -465,49 +487,56 @@ export const checkUpdate = (data: any, msgIdSet: any, apiSeq: any, settlementDet return } // let key: any = null - exchangeItem = tag.list.find((item:any)=>item.code === "exchange") - if(!exchangeItem){ + exchangeItem = tag.list.find((item: any) => item.code === "exchange") + if (!exchangeItem) { updtObj.error = `exchange tag is required for flow: ${flow}` - return - }else{ + return + } else { const exchangeValue = exchangeItem.value !['yes', 'no'].includes(exchangeValue) updtObj["exchangeNA"] = '"exchange" value must be "yes" or "no"'; - if(exchangeValue === "no"){ + if (exchangeValue === "no") { updtObj["exchangeNA"] = `"exchange" value must be "yes" for flow: ${flow}`; - return + return } - else{ - const returnItem = tag.list.find((item:any)=>item.code === "item_id").value; - console.log("returnItem",returnItem); - - let itemEligible = onSearchItems.find((item:any)=>item.id === returnItem) - console.log("itemEligible",itemEligible); - if(!itemEligible){ + else { + const returnItem = tag.list.find((item: any) => item.code === "item_id").value; + console.log("returnItem", returnItem); + + let itemEligible = onSearchItems.find((item: any) => item.id === returnItem) + console.log("itemEligible", itemEligible); + if (!itemEligible) { updtObj["itemNA"] = `Item with id: ${returnItem} not eligible for exchange as not found in on_search catalogue` return } - const {category_id} = itemEligible + const { category_id } = itemEligible const { result, missingMandatoryFields } = extractValidFieldsForCategory( category_id, tag.list, electronicsData ); - console.log("result, missingMandatoryFields ",result, missingMandatoryFields ); - + console.log("result, missingMandatoryFields ", result, missingMandatoryFields); + } } } }) }) - + } } catch (error) { - + + } + + if (schemaValidation === true) { + return { schemaErrors, businessErrors: {} } + } else if (schemaValidation === false) { + return { schemaErrors: {}, businessErrors: updtObj } + } else { + return { schemaErrors, businessErrors: updtObj } } - return updtObj } catch (error: any) { logger.error(`!!Some error occurred while checking /${apiSeq} API`, error.stack) }