diff --git a/src/controllers/llmo/llmo.js b/src/controllers/llmo/llmo.js index 619d9d9e9..7aa1ab47c 100644 --- a/src/controllers/llmo/llmo.js +++ b/src/controllers/llmo/llmo.js @@ -968,6 +968,39 @@ function LlmoController(ctx) { } }; + const getGeographicAvailability = async (context) => { + const { log, env } = context; + const { dataSource } = context.params; + + try { + const url = new URL(`${LLMO_SHEETDATA_SOURCE_URL}/geographic-availability/${dataSource}`); + + if (!env.LLMO_HLX_API_KEY) { + throw new Error('LLMO_HLX_API_KEY environment variable is not configured'); + } + + log.info(`Fetching geographic availability: ${url.toString()}`); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `token ${env.LLMO_HLX_API_KEY}`, + 'User-Agent': SPACECAT_USER_AGENT, + }, + }); + + if (!response.ok) { + log.error(`Failed to fetch geographic availability: ${response.status} ${response.statusText}`); + throw new Error(`External API returned ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return ok(data); + } catch (error) { + log.error(`Error fetching geographic availability for ${dataSource}: ${error.message}`); + return badRequest(error.message); + } + }; + const queryFiles = async (context) => { const { log } = context; const { siteId } = context.params; @@ -1706,6 +1739,7 @@ function LlmoController(ctx) { onboardCustomer, offboardCustomer, queryFiles, + getGeographicAvailability, getLlmoRationale, getBrandClaims, createOrUpdateEdgeConfig, diff --git a/src/routes/index.js b/src/routes/index.js index 9cd399b59..48d5c51ba 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -381,6 +381,9 @@ export default function getRouteHandlers( 'POST /sites/:siteId/llmo/edge-optimize-routing': llmoController.updateEdgeOptimizeCDNRouting, 'PUT /sites/:siteId/llmo/opportunities-reviewed': llmoController.markOpportunitiesReviewed, + // Geographic Availability (global, not site-specific) + 'GET /geographic-availability/:dataSource': llmoController.getGeographicAvailability, + // Tier Specific Routes 'GET /sites/:siteId/user-activities': userActivityController.getBySiteID, 'POST /sites/:siteId/user-activities': userActivityController.createTrialUserActivity, diff --git a/test/controllers/llmo/llmo.test.js b/test/controllers/llmo/llmo.test.js index 7241856b5..4c606ac62 100644 --- a/test/controllers/llmo/llmo.test.js +++ b/test/controllers/llmo/llmo.test.js @@ -5846,4 +5846,69 @@ describe('LlmoController', () => { expect(result.status).to.equal(404); }); }); + + describe('getGeographicAvailability', () => { + beforeEach(() => { + mockContext.params = { dataSource: 'countries-aimode.json' }; + }); + + it('should return geographic availability data successfully', async () => { + const mockData = { + countries: { + total: 2, + offset: 0, + limit: 100, + data: [ + { Code: 'US', Name: 'United States' }, + { Code: 'DE', Name: 'Germany' }, + ], + }, + }; + + tracingFetchStub.resolves(createMockResponse(mockData)); + + const result = await controller.getGeographicAvailability(mockContext); + expect(result.status).to.equal(200); + + const body = await result.json(); + expect(body.countries.data).to.have.lengthOf(2); + expect(tracingFetchStub).to.have.been.calledOnce; + + const fetchUrl = tracingFetchStub.firstCall.args[0]; + expect(fetchUrl).to.equal(`${EXTERNAL_API_BASE_URL}/geographic-availability/countries-aimode.json`); + + const fetchOptions = tracingFetchStub.firstCall.args[1]; + expect(fetchOptions.headers.Authorization).to.equal(`token ${TEST_API_KEY}`); + }); + + it('should return 400 when LLMO_HLX_API_KEY is not configured', async () => { + mockContext.env = { ...mockEnv, LLMO_HLX_API_KEY: undefined }; + + const result = await controller.getGeographicAvailability(mockContext); + expect(result.status).to.equal(400); + + const body = await result.json(); + expect(body.message).to.equal('LLMO_HLX_API_KEY environment variable is not configured'); + }); + + it('should return 400 when external API returns an error', async () => { + tracingFetchStub.resolves(createMockResponse(null, false, 404)); + + const result = await controller.getGeographicAvailability(mockContext); + expect(result.status).to.equal(400); + + const body = await result.json(); + expect(body.message).to.equal('External API returned 404: Not Found'); + }); + + it('should return 400 when fetch throws an error', async () => { + tracingFetchStub.rejects(new Error('Network error')); + + const result = await controller.getGeographicAvailability(mockContext); + expect(result.status).to.equal(400); + + const body = await result.json(); + expect(body.message).to.equal('Network error'); + }); + }); }); diff --git a/test/routes/index.test.js b/test/routes/index.test.js index 6c17c3469..8f6a23b6c 100755 --- a/test/routes/index.test.js +++ b/test/routes/index.test.js @@ -270,6 +270,7 @@ describe('getRouteHandlers', () => { onboardCustomer: () => null, offboardCustomer: () => null, queryFiles: () => null, + getGeographicAvailability: () => null, getLlmoRationale: () => null, getBrandClaims: () => null, createOrUpdateEdgeConfig: () => null, @@ -660,6 +661,7 @@ describe('getRouteHandlers', () => { 'GET /sites/:siteId/llmo/edge-optimize-status', 'POST /sites/:siteId/llmo/edge-optimize-routing', 'PUT /sites/:siteId/llmo/opportunities-reviewed', + 'GET /geographic-availability/:dataSource', 'GET /sites/:siteId/llmo/strategy', 'PUT /sites/:siteId/llmo/strategy', 'GET /consent-banner/:jobId', @@ -896,6 +898,8 @@ describe('getRouteHandlers', () => { expect(dynamicRoutes['POST /sites/:siteId/llmo/edge-optimize-routing'].paramNames).to.deep.equal(['siteId']); expect(dynamicRoutes['PUT /sites/:siteId/llmo/opportunities-reviewed'].handler).to.equal(mockLlmoController.markOpportunitiesReviewed); expect(dynamicRoutes['PUT /sites/:siteId/llmo/opportunities-reviewed'].paramNames).to.deep.equal(['siteId']); + expect(dynamicRoutes['GET /geographic-availability/:dataSource'].handler).to.equal(mockLlmoController.getGeographicAvailability); + expect(dynamicRoutes['GET /geographic-availability/:dataSource'].paramNames).to.deep.equal(['dataSource']); expect(dynamicRoutes['GET /sites/:siteId/llmo/strategy'].handler).to.equal(mockLlmoController.getStrategy); expect(dynamicRoutes['GET /sites/:siteId/llmo/strategy'].paramNames).to.deep.equal(['siteId']); expect(dynamicRoutes['PUT /sites/:siteId/llmo/strategy'].handler).to.equal(mockLlmoController.saveStrategy);