Skip to content

Commit

Permalink
Merge pull request #19 from Tutortoise/feat/recommendation
Browse files Browse the repository at this point in the history
feat(recommendation): integration with recommender service
  • Loading branch information
elskow authored Dec 10, 2024
2 parents 6b7af98 + d6c897b commit 293442a
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 10 deletions.
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

0 comments on commit 293442a

Please sign in to comment.