diff --git a/packages/api/src/controllers/sensorController.js b/packages/api/src/controllers/sensorController.js index 4d4e8a8271..170cb822a5 100644 --- a/packages/api/src/controllers/sensorController.js +++ b/packages/api/src/controllers/sensorController.js @@ -29,9 +29,9 @@ import PartnerReadingTypeModel from '../models/PartnerReadingTypeModel.js'; import { transaction, Model } from 'objection'; import { - createOrganization, - registerOrganizationWebhook, - bulkSensorClaim, + ENSEMBLE_BRAND, + extractEsids, + registerFarmAndClaimSensors, unclaimSensor, ENSEMBLE_UNITS_MAPPING, } from '../util/ensemble.js'; @@ -119,7 +119,7 @@ const sensorController = { const { user_id } = req.auth; try { const { access_token } = await IntegratingPartnersModel.getAccessAndRefreshTokens( - 'Ensemble Scientific', + ENSEMBLE_BRAND, ); //TODO: LF-4443 - Sensor should not use User language (unrestricted string), accept as body param or farm level detail @@ -173,65 +173,59 @@ const sensorController = { }, ); } else { - const esids = data.reduce((previous, current) => { - if (current.brand === 'Ensemble Scientific' && current.external_id) { - previous.push(current.external_id); - } - return previous; - }, []); + // Extract Ensemble Scientific sensor IDs (esids) + const esids = extractEsids(data); + let success = []; let already_owned = []; let does_not_exist = []; let occupied = []; - if (esids.length > 0) { - const organization = await createOrganization(farm_id, access_token); - - // register webhook for sensor readings - await registerOrganizationWebhook(farm_id, organization.organization_uuid, access_token); - // Register sensors with Ensemble - ({ success, already_owned, does_not_exist, occupied } = await bulkSensorClaim( + if (esids.length > 0) { + ({ success, already_owned, does_not_exist, occupied } = await registerFarmAndClaimSensors( + farm_id, access_token, - organization.organization_uuid, esids, )); } - // register organization - - // Filter sensors by those successfully registered and those with errors - const { registeredSensors, errorSensors } = data.reduce( - (prev, curr, idx) => { - if (success?.includes(curr.external_id) || already_owned?.includes(curr.external_id)) { - prev.registeredSensors.push(curr); - } else if (curr.brand !== 'Ensemble Scientific') { - prev.registeredSensors.push(curr); - } else if (does_not_exist?.includes(curr.external_id)) { - prev.errorSensors.push({ - row: idx + 2, - column: 'External_ID', - translation_key: sensorErrors.SENSOR_DOES_NOT_EXIST, - variables: { sensorId: curr.external_id }, - }); - } else if (occupied?.includes(curr.external_id)) { - prev.errorSensors.push({ - row: idx + 2, - column: 'External_ID', - translation_key: sensorErrors.SENSOR_ALREADY_OCCUPIED, - variables: { sensorId: curr.external_id }, - }); - } else { - // we know that it is an ESID but for some reason it was not returned in the expected format from the API - prev.errorSensors.push({ - row: idx + 2, - column: 'External_ID', - translation_key: sensorErrors.INTERNAL_ERROR, - variables: { sensorId: curr.external_id }, - }); - } - return prev; - }, - { registeredSensors: [], errorSensors: [] }, - ); + + const registeredSensors = []; + const errorSensors = []; + + // Iterate over each sensor in the data array + data.forEach((sensor, index) => { + if (sensor.brand !== ENSEMBLE_BRAND) { + // All non-ESCI sensors should be considered successfully registered + registeredSensors.push(sensor); + } else if ( + success.includes(sensor.external_id) || + already_owned.includes(sensor.external_id) + ) { + registeredSensors.push(sensor); + } else if (does_not_exist.includes(sensor.external_id)) { + errorSensors.push({ + row: index + 2, + column: 'External_ID', + translation_key: sensorErrors.SENSOR_DOES_NOT_EXIST, + variables: { sensorId: sensor.external_id }, + }); + } else if (occupied.includes(sensor.external_id)) { + errorSensors.push({ + row: index + 2, + column: 'External_ID', + translation_key: sensorErrors.SENSOR_ALREADY_OCCUPIED, + variables: { sensorId: sensor.external_id }, + }); + } else { + // We know that it is an ESID but it was not returned in the expected format from the API + errorSensors.push({ + row: index + 2, + column: 'External_ID', + translation_key: sensorErrors.INTERNAL_ERROR, + variables: { sensorId: sensor.external_id }, + }); + } + }); // Save sensors in database const sensorLocations = []; @@ -630,7 +624,7 @@ const sensorController = { const user_id = req.auth.user_id; const { access_token } = await IntegratingPartnersModel.getAccessAndRefreshTokens( - 'Ensemble Scientific', + ENSEMBLE_BRAND, ); let unclaimResponse; if (partner_name != 'No Integrating Partner' && external_id != '') { diff --git a/packages/api/src/util/ensemble.js b/packages/api/src/util/ensemble.js index b982f28943..95a28c8720 100644 --- a/packages/api/src/util/ensemble.js +++ b/packages/api/src/util/ensemble.js @@ -61,6 +61,26 @@ const ENSEMBLE_UNITS_MAPPING = { }, }; +const ENSEMBLE_BRAND = 'Ensemble Scientific'; + +// Return Ensemble Scientific IDs (esids) from sensor data +const extractEsids = (data) => + data + .filter((sensor) => sensor.brand === 'Ensemble Scientific' && sensor.external_id) + .map((sensor) => sensor.external_id); + +// Function to encapsulate the logic for claiming sensors +async function registerFarmAndClaimSensors(farm_id, access_token, esids) { + // Register farm as an organization with Ensemble + const organization = await createOrganization(farm_id, access_token); + + // Create a webhook for the organization + await registerOrganizationWebhook(farm_id, organization.organization_uuid, access_token); + + // Register sensors with Ensemble and return Ensemble API results + return await bulkSensorClaim(access_token, organization.organization_uuid, esids); +} + /** * Sends a request to the Ensemble API for an organization to claim sensors * @param {String} accessToken - a JWT token for accessing the Ensemble API @@ -76,6 +96,7 @@ async function bulkSensorClaim(accessToken, organizationId, esids) { data: { esids }, }; + // partial or complete failures (at least some esids failed to claim) const onError = (error) => { if (error.response?.data && error.response?.status) { return { ...error.response.data, status: error.response.status }; @@ -84,6 +105,7 @@ async function bulkSensorClaim(accessToken, organizationId, esids) { } }; + // full success (all esids successfully claimed) const onResponse = (response) => { return { success: esids, @@ -111,27 +133,27 @@ async function registerOrganizationWebhook(farmId, organizationId, accessToken) .where({ farm_id: farmId, partner_id: 1 }) .first(); if (existingIntegration?.webhook_id) { - return existingIntegration.webhook_id; - } else { - const axiosObject = { - method: 'post', - url: `${ensembleAPI}/organizations/${organizationId}/webhooks/`, - data: { - url: `${baseUrl}/sensor/reading/partner/1/farm/${farmId}`, - authorization_header: authHeader, - frequency: 15, - }, - }; - const onError = (error) => { - console.log(error); - throw new Error('Failed to register webhook with ESCI'); - }; - const onResponse = async (response) => { - await FarmExternalIntegrationsModel.updateWebhookId(farmId, response.data.id); - return { ...response.data, status: response.status }; - }; - return await ensembleAPICall(accessToken, axiosObject, onError, onResponse); + return; } + + const axiosObject = { + method: 'post', + url: `${ensembleAPI}/organizations/${organizationId}/webhooks/`, + data: { + url: `${baseUrl}/sensor/reading/partner/1/farm/${farmId}`, + authorization_header: authHeader, + frequency: 15, + }, + }; + const onError = (error) => { + console.log(error); + throw new Error('Failed to register webhook with ESCI'); + }; + const onResponse = async (response) => { + await FarmExternalIntegrationsModel.updateWebhookId(farmId, response.data.id); + return { ...response.data, status: response.status }; + }; + await ensembleAPICall(accessToken, axiosObject, onError, onResponse); } /** @@ -250,12 +272,10 @@ function isAuthError(error) { */ async function refreshTokens() { try { - const { refresh_token } = await IntegratingPartners.getAccessAndRefreshTokens( - 'Ensemble Scientific', - ); + const { refresh_token } = await IntegratingPartners.getAccessAndRefreshTokens(ENSEMBLE_BRAND); const response = await axios.post(ensembleAPI + '/token/refresh/', { refresh: refresh_token }); await IntegratingPartners.patchAccessAndRefreshTokens( - 'Ensemble Scientific', + ENSEMBLE_BRAND, response.data?.access, response.data?.access, ); @@ -281,7 +301,7 @@ async function authenticateToGetTokens() { const password = process.env.ENSEMBLE_PASSWORD; const response = await axios.post(ensembleAPI + '/token/', { username, password }); await IntegratingPartners.patchAccessAndRefreshTokens( - 'Ensemble Scientific', + ENSEMBLE_BRAND, response.data?.access, response.data?.access, ); @@ -330,9 +350,9 @@ async function unclaimSensor(org_id, external_id, access_token) { } export { - bulkSensorClaim, - registerOrganizationWebhook, - createOrganization, + ENSEMBLE_BRAND, + extractEsids, + registerFarmAndClaimSensors, unclaimSensor, ENSEMBLE_UNITS_MAPPING, };