Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(recommendation): integration with recommender service #19

Merged
merged 2 commits into from
Dec 10, 2024
Merged
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
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ export const FACE_VALIDATION_ENABLED = !!FACE_VALIDATION_URL;

export const ABUSIVE_DETECTION_URL = process.env.ABUSIVE_DETECTION_URL;
export const ABUSIVE_DETECTION_ENABLED = !!ABUSIVE_DETECTION_URL;

export const RECOMMENDATION_URL = process.env.RECOMMENDATION_URL;
export const RECOMMENDATION_ENABLED = !!RECOMMENDATION_URL;
35 changes: 26 additions & 9 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { FaceValidationService } from "./module/face-validation/face-validation.
import { createFaceValidationService } from "./module/face-validation/face-validation.factory";
import { AbusiveDetectionService } from "./module/abusive-detection/abusive-detection.interface";
import { createAbusiveDetectionService } from "@/module/abusive-detection/abusive-detection.factory";
import { createRecommendationService } from "./module/recommendation/recommendation.factory";
import { RecommendationService } from "./module/recommendation/recommendation.interface";

interface Container {
authRepository: AuthRepository;
Expand All @@ -25,24 +27,39 @@ interface Container {
reviewRepository: ReviewRepository;
faceValidationService: FaceValidationService;
abusiveDetectionService: AbusiveDetectionService;
recommendationService: RecommendationService;
}

let containerInstance: Container | null = null;

export const setupContainer = (): Container => {
if (!containerInstance) {
const authRepository = new AuthRepository(db);
const categoryRepository = new CategoryRepository(db);
const learnerRepository = new LearnerRepository(db);
const tutorRepository = new TutorRepository(db);
const tutoriesRepository = new TutoriesRepository(db);
const orderRepository = new OrderRepository(db);
const chatRepository = new ChatRepository(db);
const fcmRepository = new FCMRepository(db);
const reviewRepository = new ReviewRepository(db);

containerInstance = {
authRepository: new AuthRepository(db),
categoryRepository: new CategoryRepository(db),
learnerRepository: new LearnerRepository(db),
tutorRepository: new TutorRepository(db),
tutoriesRepository: new TutoriesRepository(db),
orderRepository: new OrderRepository(db),
chatRepository: new ChatRepository(db),
fcmRepository: new FCMRepository(db),
reviewRepository: new ReviewRepository(db),
authRepository,
categoryRepository,
learnerRepository,
tutorRepository,
tutoriesRepository,
orderRepository,
chatRepository,
fcmRepository,
reviewRepository,
faceValidationService: createFaceValidationService(),
abusiveDetectionService: createAbusiveDetectionService(),
recommendationService: createRecommendationService(
learnerRepository,
tutoriesRepository,
),
};
}

Expand Down
47 changes: 47 additions & 0 deletions src/module/recommendation/recommendation.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { RECOMMENDATION_ENABLED, RECOMMENDATION_URL } from "@/config";
import {
RemoteRecommendationService,
InterestRecommendationService,
} from "./recommendation.service";
import { logger } from "@middleware/logging.middleware";
import axios from "axios";
import { LearnerRepository } from "../learner/learner.repository";
import { TutoriesRepository } from "../tutories/tutories.repository";

export const createRecommendationService = (
learnerRepository: LearnerRepository,
tutoriesRepository: TutoriesRepository,
) => {
if (process.env.NODE_ENV === "test" || !RECOMMENDATION_ENABLED) {
logger.info(
"Recommendation service is disabled, using simple interest-matching service",
);
return new InterestRecommendationService(
learnerRepository,
tutoriesRepository,
);
}

const service = new RemoteRecommendationService(RECOMMENDATION_URL!);

try {
axios
.get(`${RECOMMENDATION_URL}/health`, {
timeout: 5000,
})
.catch(() => {
throw new Error("Health check failed");
});

logger.info("Recommendation service is healthy and ready");
return service;
} catch (error) {
logger.warn(
`Recommendation service is unavailable: ${error}, falling back to simple interest-matching service`,
);
return new InterestRecommendationService(
learnerRepository,
tutoriesRepository,
);
}
};
43 changes: 43 additions & 0 deletions src/module/recommendation/recommendation.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TutorAvailability } from "@/db/schema";
import { LearningStyle } from "../learner/learner.types";

export interface Recommendation {
tutor_id: string;
tutories_id: string;
name: string;
email: string;
city: string | null;
district: string | null;
category: string;
tutory_name: string;
about: string;
methodology: string;
hourly_rate: number;
type_lesson: string;
completed_orders: number;
total_orders: number;
match_reasons?: string[];
location_match: boolean;
availability: TutorAvailability | null;
}

export interface RecommendationServiceResponse {
learner: {
id: string;
name: string;
email: string;
learning_style: LearningStyle | null;
city: string | null;
district: string | null;
interests: string[];
};
recommendations: Recommendation[];
total_found: number;
requested: number;
}

export interface RecommendationService {
getRecommendations(learnerId: string): Promise<RecommendationServiceResponse>;
trackInteraction(learnerId: string, tutoriesId: string): Promise<void>;
checkHealth(): Promise<boolean>;
}
111 changes: 111 additions & 0 deletions src/module/recommendation/recommendation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import axios from "axios";
import {
Recommendation,
RecommendationService,
RecommendationServiceResponse,
} from "./recommendation.interface";
import { logger } from "@middleware/logging.middleware";
import { TutoriesRepository } from "../tutories/tutories.repository";
import { LearnerRepository } from "../learner/learner.repository";

export class RemoteRecommendationService implements RecommendationService {
constructor(private readonly baseUrl: string) {}

async trackInteraction(learnerId: string, tutoriesId: string): Promise<void> {
try {
axios.get(`${this.baseUrl}/interaction/${learnerId}/${tutoriesId}`);
} catch (error) {
logger.error("Recommendation service error:", error);
throw new Error("Recommendation service unavailable");
}
}

async getRecommendations(
learnerId: string,
): Promise<RecommendationServiceResponse> {
try {
const response = await axios.get<RecommendationServiceResponse>(
`${this.baseUrl}/recommendations/${learnerId}`,
{ timeout: 5000 },
);

return response.data;
} catch (error) {
logger.error("Recommendation service error:", error);
throw new Error("Recommendation service unavailable");
}
}

checkHealthSync(): boolean {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", `${this.baseUrl}/health`, false); // Make the request synchronous
xhr.timeout = 5000;
xhr.send(null);

return xhr.status === 200;
} catch (error) {
logger.warn("Health check failed:", error);
return false;
}
}

async checkHealth(): Promise<boolean> {
try {
const response = await axios.get(`${this.baseUrl}/health`, {
timeout: 5000,
});
return response.data.status === "healthy";
} catch (error) {
return false;
}
}
}

export class InterestRecommendationService implements RecommendationService {
constructor(
private readonly learnerRepository: LearnerRepository,
private readonly tutoriesRepository: TutoriesRepository,
) {}

async trackInteraction(learnerId: string, tutoriesId: string): Promise<void> {
// No-op
}

async getRecommendations(
learnerId: string,
): Promise<RecommendationServiceResponse> {
const learner = await this.learnerRepository.getLearnerById(learnerId);
if (!learner) {
throw new Error("Learner not found");
}

const recommendations: Recommendation[] =
await this.tutoriesRepository.getTutoriesByLearnerInterests(learnerId);

const result: RecommendationServiceResponse = {
learner: {
id: learner.id,
name: learner.name,
email: learner.email,
learning_style: learner.learningStyle,
city: learner.city,
district: learner.district,
interests: learner.interests,
},
recommendations,
total_found: 0,
requested: 0,
};

return result;
}

checkHealthSync(): boolean {
return true;
}

async checkHealth(): Promise<boolean> {
return true;
}
}
52 changes: 52 additions & 0 deletions src/module/tutories/tutories.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
createTutoriesSchema,
deleteTutoriesSchema,
getAverageRateSchema,
getRecommendationsSchema,
getServiceSchema,
getTutoriesSchema,
trackInteractionSchema,
updateTutoriesSchema,
} from "@/module/tutories/tutories.schema";
import { TutoriesService } from "@/module/tutories/tutories.service";
Expand All @@ -18,6 +20,7 @@ const tutoriesService = new TutoriesService({
tutorRepository: container.tutorRepository,
reviewRepository: container.reviewRepository,
abusiveDetection: container.abusiveDetectionService,
recommender: container.recommendationService,
});

type GetTutoriesSchema = z.infer<typeof getTutoriesSchema>;
Expand Down Expand Up @@ -276,3 +279,52 @@ export const deleteTutories: Controller<DeleteTutorServiceSchema> = async (
});
}
};

type GetRecommendationsSchema = z.infer<typeof getRecommendationsSchema>;
export const getRecommendations: Controller<GetRecommendationsSchema> = async (
req,
res,
) => {
const learnerId = req.params.learnerId;

try {
const recommendations = await tutoriesService.getRecommendations(learnerId);

res.json({
status: "success",
data: recommendations,
});
} catch (error) {
logger.error(`Failed to get recommendations: ${error}`);

res.status(500).json({
status: "error",
message: `Failed to get recommendations`,
});
}
};

type TrackInteractionSchema = z.infer<typeof trackInteractionSchema>;
export const trackInteraction: Controller<TrackInteractionSchema> = async (
req,
res,
) => {
const learnerId = req.params.learnerId;
const tutoriesId = req.params.tutoriesId;

try {
await tutoriesService.trackInteraction(learnerId, tutoriesId);

res.json({
status: "success",
message: "Interaction tracked successfully",
});
} catch (error) {
logger.error(`Failed to track interaction: ${error}`);

res.status(500).json({
status: "error",
message: `Failed to track interaction`,
});
}
};
Loading
Loading