From 726242033568ddd2ff2a0a73469ec395473a1581 Mon Sep 17 00:00:00 2001 From: Michelle Inez Date: Thu, 19 Dec 2024 11:08:09 -0800 Subject: [PATCH 1/3] feat: erl email integration with loanRecommendations endpoint --- server/live-loan-router.js | 40 ++++++++++++------- server/util/live-loan/live-loan-fetch.js | 33 +++++++++++++-- .../unit/specs/server/live-loan-fetch.spec.js | 18 ++++++++- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/server/live-loan-router.js b/server/live-loan-router.js index cb7bece8ee..f922e7b7b7 100644 --- a/server/live-loan-router.js +++ b/server/live-loan-router.js @@ -2,11 +2,12 @@ import express from 'express'; import { error } from './util/log.js'; import { getFromCache, setToCache } from './util/memJsUtils.js'; import drawLoanCard from './util/live-loan/live-loan-draw.js'; -import fetchLoansByType from './util/live-loan/live-loan-fetch.js'; +import fetchLoansByType, { QUERY_TYPE } from './util/live-loan/live-loan-fetch.js'; import { trace } from './util/mockTrace.js'; -async function fetchRecommendedLoans(type, id, cache, flss = false) { - const loanCachedName = flss ? `recommendations-by-${type}-id-${id}-flss` : `recommendations-by-${type}-id-${id}`; +async function fetchRecommendedLoans(type, id, cache, queryType = QUERY_TYPE.DEFAULT) { + const queryTypeSuffix = queryType !== QUERY_TYPE.DEFAULT ? `-${queryType}` : ''; + const loanCachedName = `recommendations-by-${type}-id-${id}${queryTypeSuffix}`; // If we have loan data in memjscache return that quickly try { const cachedLoanData = await getFromCache(loanCachedName, cache); @@ -18,7 +19,7 @@ async function fetchRecommendedLoans(type, id, cache, flss = false) { } // Otherwise we need to hit the graphql endpoint. - const loanData = await trace('fetchLoansByType', { resource: type }, async () => fetchLoansByType(type, id, flss)); // eslint-disable-line max-len + const loanData = await trace('fetchLoansByType', { resource: type }, async () => fetchLoansByType(type, id, queryType === QUERY_TYPE.FLSS, queryType === QUERY_TYPE.RECOMMENDATIONS)); // eslint-disable-line max-len // Set the loan data in memcache, return the loan data if (loanData && loanData.length) { @@ -34,12 +35,15 @@ async function fetchRecommendedLoans(type, id, cache, flss = false) { throw new Error('No loans returned'); } -async function getLoanForRequest(type, cache, req, flss = false) { +async function getLoanForRequest(type, cache, req, queryType = QUERY_TYPE.DEFAULT) { // Use default values for id and offset if they are not numeric const id = req.params?.id || 0; const offset = req.params?.offset || 1; - const loanData = await trace('fetchRecommendedLoans', async () => fetchRecommendedLoans(type, id, cache, flss)); + const loanData = await trace( + 'fetchRecommendedLoans', + async () => fetchRecommendedLoans(type, id, cache, queryType) + ); // if there are fewer loan results than the offset, return the last result if (offset > loanData.length) { @@ -48,9 +52,9 @@ async function getLoanForRequest(type, cache, req, flss = false) { return loanData[offset - 1]; } -async function redirectToUrl(type, cache, req, res, flss = false) { +async function redirectToUrl(type, cache, req, res, queryType = QUERY_TYPE.DEFAULT) { try { - const loan = await trace('getLoanForRequest', async () => getLoanForRequest(type, cache, req, flss)); + const loan = await trace('getLoanForRequest', async () => getLoanForRequest(type, cache, req, queryType)); // Standard destination is the borrower profile page let redirect = `/lend/${loan.id}`; // If the original request had query params on it, forward those along @@ -72,11 +76,12 @@ async function redirectToUrl(type, cache, req, res, flss = false) { } } -async function serveImg(type, style, cache, req, res, flss = false) { +async function serveImg(type, style, cache, req, res, queryType = QUERY_TYPE.DEFAULT) { try { - const loan = await trace('getLoanForRequest', async () => getLoanForRequest(type, cache, req, flss)); + const loan = await trace('getLoanForRequest', async () => getLoanForRequest(type, cache, req, queryType)); let loanImg; - const imgCachedName = flss ? `loan-card-img-${style}-${loan.id}-flss` : `loan-card-img-${style}-${loan.id}`; + const queryTypeSuffix = queryType !== QUERY_TYPE.DEFAULT ? `-${queryType}` : ''; + const imgCachedName = `loan-card-img-${style}-${loan.id}${queryTypeSuffix}`; const cachedLoanImg = await getFromCache(imgCachedName, cache); if (cachedLoanImg) { loanImg = cachedLoanImg; @@ -139,21 +144,28 @@ export default function liveLoanRouter(cache) { // User URL Router FLSS router.use('/flss/u/:id(\\d{0,})/url/:offset(\\d{0,})', async (req, res) => { await trace('live-loan.flss.user.redirectToUrl', { resource: req.path }, async () => { - await redirectToUrl('user', cache, req, res, true); + await redirectToUrl('user', cache, req, res, QUERY_TYPE.FLSS); }); }); // User IMG Router FLSS (Legacy) router.use('/flss/u/:id(\\d{0,})/img/:offset(\\d{0,})', async (req, res) => { await trace('live-loan.flss.user.serveImg', { resource: req.path }, async () => { - await serveImg('user', 'legacy', cache, req, res, true); + await serveImg('user', 'legacy', cache, req, res, QUERY_TYPE.FLSS); }); }); // User IMG Router FLSS (Kiva Classic) router.use('/flss/u/:id(\\d{0,})/img2/:offset(\\d{0,})', async (req, res) => { await trace('live-loan.flss.user.serveImg', { resource: req.path }, async () => { - await serveImg('user', 'classic', cache, req, res, true); + await serveImg('user', 'classic', cache, req, res, QUERY_TYPE.FLSS); + }); + }); + + // User IMG Router Recommendations (Kiva Classic) + router.use('/recommendations/u/:id(\\d{0,})/img2/:offset(\\d{0,})', async (req, res) => { + await trace('live-loan.recommendations.user.serveImg', { resource: req.path }, async () => { + await serveImg('user', 'classic', cache, req, res, QUERY_TYPE.RECOMMENDATIONS); }); }); diff --git a/server/util/live-loan/live-loan-fetch.js b/server/util/live-loan/live-loan-fetch.js index 3843ed8210..2ce6469dfa 100644 --- a/server/util/live-loan/live-loan-fetch.js +++ b/server/util/live-loan/live-loan-fetch.js @@ -1,6 +1,12 @@ import fetchGraphQL from '../fetchGraphQL.js'; import { warn, error } from '../log.js'; +export const QUERY_TYPE = { + DEFAULT: 'default', + FLSS: 'flss', + RECOMMENDATIONS: 'recommendations' +}; + // Number of loans to fetch const loanCount = 4; @@ -60,8 +66,27 @@ async function fetchLoansFromGraphQL(request, resultPath) { } // Get per-user recommended loans from the ML service -async function fetchRecommendationsByLoginId(id, flss = false) { - if (flss) { +async function fetchRecommendationsByLoginId(id, queryType = QUERY_TYPE.DEFAULT) { + if (queryType === QUERY_TYPE.RECOMMENDATIONS) { + return fetchLoansFromGraphQL( + { + query: `query($userId: Int, $limit: Int) { + loanRecommendations( + userId: $userId, + limit: ${loanCount}, + origin: "email:live-loans" + ) { + ${loanValues} + } + }`, + variables: { + userId: Number(id), + limit: loanCount + } + }, + 'data.loanRecommendations.values' + ); + } if (queryType === QUERY_TYPE.FLSS) { return fetchLoansFromGraphQL( { query: `query($userId: Int) { @@ -495,9 +520,9 @@ const shouldUseFLSS = async filterString => { }; // Export a function that will fetch loans by live-loan type and id -export default async function fetchLoansByType(type, id, flss = false) { +export default async function fetchLoansByType(type, id, queryType = QUERY_TYPE.DEFAULT) { if (type === 'user') { - return fetchRecommendationsByLoginId(id, flss); + return fetchRecommendationsByLoginId(id, queryType); } if (type === 'loan') { return fetchRecommendationsByLoanId(id); } if (type === 'filter') { diff --git a/test/unit/specs/server/live-loan-fetch.spec.js b/test/unit/specs/server/live-loan-fetch.spec.js index 4bf93647ca..d9ccf8f3b1 100644 --- a/test/unit/specs/server/live-loan-fetch.spec.js +++ b/test/unit/specs/server/live-loan-fetch.spec.js @@ -163,7 +163,7 @@ describe('live-loan-fetch', () => { fetch.default.mockClear(); fetch.default.mockResolvedValue({ json: () => { } }); - await fetchLoansByType.default('user', '1234', true); + await fetchLoansByType.default('user', '1234', 'flss'); const { variables, query } = JSON.parse(fetch.default.mock.calls[0][1].body); expect(fetch).toBeDefined(); @@ -173,4 +173,20 @@ describe('live-loan-fetch', () => { expect(fetch.default.mock.results[0].value).toBeDefined(); }); }); + + describe('fetchRecommendationsByLoanRecs', () => { + it('should make recommendations call with correct parameters', async () => { + fetch.default.mockClear(); + fetch.default.mockResolvedValue({ json: () => { } }); + + await fetchLoansByType.default('user', '1234', 'recommendations'); + + const { variables, query } = JSON.parse(fetch.default.mock.calls[0][1].body); + expect(fetch).toBeDefined(); + expect(variables.userId).toEqual(1234); + expect(query).toBeDefined(); + expect(query).toContain('loanRecommendations'); + expect(fetch.default.mock.results[0].value).toBeDefined(); + }); + }); }); From c0f32ef495ee3c874ba365198e8d16e305d1ea7d Mon Sep 17 00:00:00 2001 From: Michelle Inez Date: Thu, 19 Dec 2024 11:18:26 -0800 Subject: [PATCH 2/3] feat: added support for a couple more endpoint types --- server/live-loan-router.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/live-loan-router.js b/server/live-loan-router.js index f922e7b7b7..f6d5e5530f 100644 --- a/server/live-loan-router.js +++ b/server/live-loan-router.js @@ -162,6 +162,20 @@ export default function liveLoanRouter(cache) { }); }); + // User URL Router FLSS + router.use('/recommendations/u/:id(\\d{0,})/url/:offset(\\d{0,})', async (req, res) => { + await trace('live-loan.flss.user.redirectToUrl', { resource: req.path }, async () => { + await redirectToUrl('user', cache, req, res, QUERY_TYPE.RECOMMENDATIONS); + }); + }); + + // User IMG Router Recommendations (Legacy) + router.use('/recommendations/u/:id(\\d{0,})/img/:offset(\\d{0,})', async (req, res) => { + await trace('live-loan.recommendations.user.serveImg', { resource: req.path }, async () => { + await serveImg('user', 'legacy', cache, req, res, QUERY_TYPE.RECOMMENDATIONS); + }); + }); + // User IMG Router Recommendations (Kiva Classic) router.use('/recommendations/u/:id(\\d{0,})/img2/:offset(\\d{0,})', async (req, res) => { await trace('live-loan.recommendations.user.serveImg', { resource: req.path }, async () => { From 5016ca2a39adad9bcb3b0d8f695e328c0ab1c910 Mon Sep 17 00:00:00 2001 From: Michelle Inez Date: Thu, 19 Dec 2024 13:03:57 -0800 Subject: [PATCH 3/3] fix: pr comments addressed --- server/live-loan-router.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/live-loan-router.js b/server/live-loan-router.js index f6d5e5530f..f59d8e6eb9 100644 --- a/server/live-loan-router.js +++ b/server/live-loan-router.js @@ -19,7 +19,11 @@ async function fetchRecommendedLoans(type, id, cache, queryType = QUERY_TYPE.DEF } // Otherwise we need to hit the graphql endpoint. - const loanData = await trace('fetchLoansByType', { resource: type }, async () => fetchLoansByType(type, id, queryType === QUERY_TYPE.FLSS, queryType === QUERY_TYPE.RECOMMENDATIONS)); // eslint-disable-line max-len + const loanData = await trace( + 'fetchLoansByType', + { resource: type }, + async () => fetchLoansByType(type, id, queryType) + ); // Set the loan data in memcache, return the loan data if (loanData && loanData.length) { @@ -162,7 +166,7 @@ export default function liveLoanRouter(cache) { }); }); - // User URL Router FLSS + // User URL Router Recommendations router.use('/recommendations/u/:id(\\d{0,})/url/:offset(\\d{0,})', async (req, res) => { await trace('live-loan.flss.user.redirectToUrl', { resource: req.path }, async () => { await redirectToUrl('user', cache, req, res, QUERY_TYPE.RECOMMENDATIONS);