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

Food hygiene rating scheme #179

Merged
merged 9 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions src/components/FeaturePanel/FeaturePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const featuredKeys = [
'contact:mobile',
'opening_hours',
'description',
'fhrs:id',
];

const FeaturePanel = () => {
Expand Down
6 changes: 5 additions & 1 deletion src/components/FeaturePanel/FeaturedTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import WebsiteRenderer from './renderers/WebsiteRenderer';
import OpeningHoursRenderer from './renderers/OpeningHoursRenderer';
import PhoneRenderer from './renderers/PhoneRenderer';
import { EditIconButton } from './helpers/EditIconButton';
import { FoodHygieneRatingSchemeRenderer } from './renderers/FoodHygieneRatingScheme';

const Wrapper = styled.div`
position: relative;
Expand All @@ -29,13 +30,16 @@ const Value = styled.div`
`;

const DefaultRenderer = ({ v }) => v;
const renderers = {
const renderers: {
[key: string]: React.FC<{ v: string }>;
} = {
website: WebsiteRenderer,
'contact:website': WebsiteRenderer,
phone: PhoneRenderer,
'contact:phone': PhoneRenderer,
'contact:mobile': PhoneRenderer,
opening_hours: OpeningHoursRenderer,
'fhrs:id': FoodHygieneRatingSchemeRenderer,
};

export const FeaturedTag = ({ k, v, onEdit }) => {
Expand Down
85 changes: 85 additions & 0 deletions src/components/FeaturePanel/renderers/FoodHygieneRatingScheme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useEffect, useState } from 'react';
import { Typography } from '@material-ui/core';
import { Restaurant } from '@material-ui/icons';
import { getEstablishmentRatingValue } from '../../../services/fhrsApi';

interface StarRatingProps {
stars: number;
maxStars: number;
}

const StarRating = ({ stars, maxStars }: StarRatingProps) => {
const starArray = new Array(maxStars).fill(0);
return (
<div>
{starArray.map((_, i) => (
<span>{i < stars ? '★' : '☆'}</span>
))}
</div>
);
};

const useLoadingState = () => {
const [rating, setRating] = useState<number>();
const [error, setError] = useState<string>();
const [loading, setLoading] = useState(true);

const finishRating = (payload) => {
setLoading(false);
setRating(payload);
};

const startRating = () => {
setLoading(true);
setRating(undefined);
setError(undefined);
};

const failRating = () => {
setError('Could not load rating');
setLoading(false);
};

return { rating, error, loading, startRating, finishRating, failRating };
};

export const FoodHygieneRatingSchemeRenderer = ({ v }) => {
const { rating, error, loading, startRating, finishRating, failRating } =
useLoadingState();

useEffect(() => {
const loadData = async () => {
startRating();
const ratingValue = await getEstablishmentRatingValue(v);
if (Number.isNaN(rating)) {
failRating();
}
finishRating(ratingValue);
};

loadData();
}, []);

return (
<>
{loading ? (
<>
<span className="dotloader" />
<span className="dotloader" />
<span className="dotloader" />
</>
) : (
<>
<Restaurant fontSize="small" />
{Number.isNaN(rating) || error ? (
<Typography color="error">No rating available</Typography>
) : (
<>
<StarRating stars={rating} maxStars={5} />
Dlurak marked this conversation as resolved.
Show resolved Hide resolved
</>
)}
</>
)}
</>
);
};
161 changes: 161 additions & 0 deletions src/services/__tests__/fhrsApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { getEstablishmentRatingValue } from '../fhrsApi';

describe('fetchRating', () => {
it('should return a rating between 0 and 5', () => {
const fhrsIds = [
'268458',
'269382',
'268811',
'268903',
'269258',
'269026',
'269099',
'269095',
'809473',
'268631',
'269312',
'269253',
'988122',
'723651',
'269229',
'268672',
'268669',
'269078',
'854391',
'708562',
'268508',
'989695',
'268628',
'268629',
'269386',
'268937',
'269058',
'268487',
'1065520',
'268323',
'268324',
'268322',
'842962',
'268874',
'269309',
'269148',
'269171',
'268488',
'777803',
'268630',
'269224',
'268617',
'268615',
'268618',
'269431',
'269384',
'269383',
'999835',
'268752',
'269248',
'269495',
'269497',
'268490',
'977892',
'268807',
'729281',
'269464',
'269158',
'848733',
'268780',
'952041',
'268481',
'269028',
'949726',
'269039',
'542347',
'1015463',
'268624',
'269252',
'268982',
'679551',
'949727',
'989697',
'406128',
'997133',
'836583',
'997140',
'743833',
'988109',
'268454',
'964599',
'1011214',
'269163',
'841948',
'889450',
'268806',
'1015461',
'268453',
'929914',
'269262',
'268351',
'269202',
'268849',
'777807',
'268441',
'997130',
'1031734',
'661417',
'269447',
'269434',
'269449',
'268866',
'269448',
'269442',
'269441',
'269436',
'269437',
'269446',
'269438',
'269452',
'269433',
'955351',
'269493',
'268355',
'269260',
'269361',
'268548',
'268352',
'269402',
'681923',
'269142',
'268353',
'269391',
'661418',
'904891',
'268433',
'268667',
'1401890',
'268356',
'268993',
'268935',
'268934',
'268933',
'268435',
'269201',
'848730',
'268461',
'268410',
'668853',
'268447',
'269155',
'268450',
'1292333',
];

fhrsIds.forEach(async (id) => {
const rating = await getEstablishmentRatingValue(parseInt(id, 10));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please mock the fetchJson to return an example response?

jest
      .spyOn(fetch, 'fetchJson')
      .mockResolvedValue(`{ response json ... }`);

Also If the response format is usally the same, one example is enough.

We write unit tests to test our conversion functions. Your test would check if API is working correctly, but if it was down it would block our pipeline. Good rule is "unit tests must work offline".

It could be good to know if shape of their API changed, but this is not role of unit tests. We could have perhaps some "3rd party service status page" checking that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I changed it now, but I am not sure if it is right to have the entire API response or if it would be right to just have the relevant parts of the response.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is perfect. We call it fixtures, and its fine to have long JSONs in tests. This test isn't that much useful, but at least we know what other info is in that api call - for future reference.

if (Number.isNaN(rating)) {
expect(rating).toBeNaN();
return;
}
expect(rating).toBeGreaterThanOrEqual(0);
expect(rating).toBeLessThanOrEqual(5);
});
});
});
16 changes: 16 additions & 0 deletions src/services/fhrsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { fetchJson } from './fetch';

function fetchEstablishmentData(id: number) {
return fetchJson(`https://api.ratings.food.gov.uk/Establishments/${id}`, {
headers: {
'x-api-version': '2',
},
});
}

export async function getEstablishmentRatingValue(id: number) {
const allData = await fetchEstablishmentData(id).catch(() => null);
const ratingString = allData?.RatingValue;
const ratingValue = parseInt(ratingString, 10);
return ratingValue;
}