Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/controllers/llmo/llmo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1706,6 +1739,7 @@ function LlmoController(ctx) {
onboardCustomer,
offboardCustomer,
queryFiles,
getGeographicAvailability,
getLlmoRationale,
getBrandClaims,
createOrUpdateEdgeConfig,
Expand Down
3 changes: 3 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions test/controllers/llmo/llmo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
4 changes: 4 additions & 0 deletions test/routes/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ describe('getRouteHandlers', () => {
onboardCustomer: () => null,
offboardCustomer: () => null,
queryFiles: () => null,
getGeographicAvailability: () => null,
getLlmoRationale: () => null,
getBrandClaims: () => null,
createOrUpdateEdgeConfig: () => null,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down