Refactor geocoding services and improve logging

- Updated the Nominatim geocoding service to use a class-based structure and accept a logger instance for better logging control.
- Modified tests for the Nominatim service to align with the new structure and improved logging assertions.
- Removed the disabled notification service test file.
- Added a new GeocodingFailedError class to handle geocoding failures more explicitly.
- Enhanced error logging in the queue service to include structured error objects.
- Updated user service to accept a logger instance for better logging in address upsert operations.
- Added request-scoped logger to Express Request interface for type-safe logging in route handlers.
- Improved logging in utility functions for better debugging and error tracking.
- Created a new GoogleGeocodingService class for Google Maps geocoding with structured logging.
- Added tests for the useAiAnalysis hook to ensure proper functionality and error handling.
This commit is contained in:
2025-12-13 17:52:30 -08:00
parent 728f4a5f7e
commit 2affda25dc
62 changed files with 1928 additions and 1329 deletions

View File

@@ -1,5 +1,6 @@
// src/services/backgroundJobService.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
// Use vi.hoisted to ensure the mock variable is available when vi.mock is executed.
const { mockCronSchedule } = vi.hoisted(() => {
@@ -14,6 +15,9 @@ vi.mock('../services/logger.server', () => ({
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
fatal: vi.fn(),
trace: vi.fn(),
silent: vi.fn(),
},
}));
@@ -56,16 +60,18 @@ describe('Background Job Service', () => {
{ user_id: 'user-2', email: 'user2@test.com', full_name: 'User Two', master_item_id: 3, item_name: 'Bread', best_price_in_cents: 250, store_name: 'Bakery', flyer_id: 103, valid_to: '2024-10-22' },
];
// This mockLogger is for the service instance, not the global one used by cron.schedule
const mockServiceLogger = {
// Helper to create a type-safe mock logger
const createMockLogger = (): Logger => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
};
child: vi.fn(() => createMockLogger()),
} as unknown as Logger);
// Instantiate the service with mock dependencies for each test run
const service = new BackgroundJobService(mockPersonalizationRepo as any, mockNotificationRepo as any, mockEmailQueue as unknown as Queue<any>, mockServiceLogger);
const mockServiceLogger = createMockLogger();
const service = new BackgroundJobService(mockPersonalizationRepo as any, mockNotificationRepo as any, mockEmailQueue as unknown as Queue<any>, mockServiceLogger as any);
it('should do nothing if no deals are found for any user', async () => {
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
@@ -125,8 +131,8 @@ describe('Background Job Service', () => {
// Check that it logged the error for user 1
expect(mockServiceLogger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
expect.stringContaining('Failed to process deals for user user-1'),
expect.any(Object)
);
// Check that it still processed user 2 successfully
@@ -141,8 +147,8 @@ describe('Background Job Service', () => {
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockRejectedValue(new Error('Critical DB Failure'));
await expect(service.runDailyDealCheck()).rejects.toThrow('Critical DB Failure');
expect(mockServiceLogger.error).toHaveBeenCalledWith(
'[BackgroundJob] A critical error occurred during the daily deal check:',
expect.any(Object)
{ error: expect.any(Error) },
'[BackgroundJob] A critical error occurred during the daily deal check:'
);
});
@@ -155,7 +161,7 @@ describe('Background Job Service', () => {
// Act & Assert
await expect(service.runDailyDealCheck()).rejects.toThrow(dbError);
expect(mockServiceLogger.error).toHaveBeenCalledWith(
'[BackgroundJob] A critical error occurred during the daily deal check:', { error: dbError }
{ err: dbError }, '[BackgroundJob] A critical error occurred during the daily deal check'
);
});
});
@@ -181,7 +187,7 @@ describe('Background Job Service', () => {
});
it('should schedule three cron jobs with the correct schedules', () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
expect(mockCronSchedule).toHaveBeenCalledTimes(3);
expect(mockCronSchedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function));
@@ -190,7 +196,7 @@ describe('Background Job Service', () => {
});
it('should call runDailyDealCheck when the first cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
// Get the callback function for the first cron job
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
@@ -202,15 +208,15 @@ describe('Background Job Service', () => {
it('should log an error and release the lock if runDailyDealCheck fails', async () => {
const jobError = new Error('Cron job failed');
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
await dailyDealCheckCallback();
expect(mockBackgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
expect(globalMockLogger.error).toHaveBeenCalledWith(
'[BackgroundJob] Cron job for daily deal check failed unexpectedly.',
{ error: jobError }
{ err: jobError },
'[BackgroundJob] Cron job for daily deal check failed unexpectedly.'
);
// It should run again, proving the lock was released in the finally block
await dailyDealCheckCallback();
@@ -224,7 +230,7 @@ describe('Background Job Service', () => {
// Make the first call hang indefinitely
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(new Promise(() => {}));
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
// Trigger the job once, it will hang
@@ -239,7 +245,7 @@ describe('Background Job Service', () => {
});
it('should enqueue an analytics job when the second cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();
@@ -250,17 +256,17 @@ describe('Background Job Service', () => {
it('should log an error if enqueuing the analytics job fails', async () => {
const queueError = new Error('Redis is down');
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();
expect(mockAnalyticsQueue.add).toHaveBeenCalledTimes(1);
expect(globalMockLogger.error).toHaveBeenCalledWith('[BackgroundJob] Failed to enqueue daily analytics job.', { error: queueError });
expect(globalMockLogger.error).toHaveBeenCalledWith({ err: queueError }, '[BackgroundJob] Failed to enqueue daily analytics job.');
});
it('should enqueue a weekly analytics job when the third cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
// The weekly job is the third one scheduled
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
@@ -276,12 +282,12 @@ describe('Background Job Service', () => {
it('should log an error if enqueuing the weekly analytics job fails', async () => {
const queueError = new Error('Redis is down for weekly job');
vi.mocked(mockWeeklyAnalyticsQueue.add).mockRejectedValue(queueError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue, globalMockLogger as any);
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
await weeklyAnalyticsJobCallback();
expect(globalMockLogger.error).toHaveBeenCalledWith('[BackgroundJob] Failed to enqueue weekly analytics job.', { error: queueError });
expect(globalMockLogger.error).toHaveBeenCalledWith({ err: queueError }, '[BackgroundJob] Failed to enqueue weekly analytics job.');
});
});
});