Skip to content

Commit

Permalink
feat: find related applications for a single employee (hl-1354) (#3321)
Browse files Browse the repository at this point in the history
* test: change test name that didn't reflect the test

* feat(backend): search related applications by using app id

* test: add case for searching related applications with app_no

* feat(handler): ui to search related applications
  • Loading branch information
sirtawast authored Sep 17, 2024
1 parent e559752 commit c0dc2e7
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 30 deletions.
41 changes: 41 additions & 0 deletions backend/benefit/applications/api/v1/search_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def get(self, request):

archived = request.query_params.get("archived") == "1" or False
search_from_archival = request.query_params.get("archival") == "1" or False
application_number = request.query_params.get("app_no")

subsidy_in_effect = request.query_params.get("subsidy_in_effect")

Expand Down Expand Up @@ -98,6 +99,7 @@ def get(self, request):
search_string,
in_memory_filter_str,
detected_pattern,
application_number,
search_from_archival,
)

Expand Down Expand Up @@ -160,8 +162,16 @@ def search_applications(
search_string,
in_memory_filter_str,
detected_pattern,
application_number=None,
search_from_archival=False,
) -> Response:
if application_number:
querysets = _query_by_application_number(
application_queryset, archival_application_queryset, application_number
)
application_queryset = querysets["application_queryset"]
archival_application_queryset = querysets["archival_application_queryset"]

if search_string == "" and in_memory_filter_str == "":
return _query_and_respond_to_empty_search(
application_queryset, archival_application_queryset
Expand Down Expand Up @@ -290,6 +300,37 @@ def _get_filter_combinations(app):
]


def _query_by_application_number(
application_queryset, archival_application_queryset, application_number
):
app = Application.objects.filter(application_number=application_number).first()

# Get year of birth from SSN, assume no-one is seeking for a job before 1900's
year_suffix = app.employee.social_security_number[4:6]
ssn_separator = app.employee.social_security_number[6:7]
ssn_separators_born_in_2000s = ["A", "B", "C", "D", "E", "F"]
year_prefix = 20 if ssn_separator in ssn_separators_born_in_2000s else 19

return {
"application_queryset": application_queryset.filter(
employee__social_security_number=app.employee.social_security_number,
company__business_id=app.company.business_id,
),
"archival_application_queryset": archival_application_queryset.filter(
Q(
employee_first_name=app.employee.first_name,
employee_last_name=app.employee.last_name,
)
& (
Q(year_of_birth=f"{year_prefix}{year_suffix}")
| Q(
year_of_birth="1900"
) # A few ArchivalApplication do not have birth year and is marked as 1900
),
),
}


def _query_and_respond_to_empty_search(
application_queryset, archival_application_queryset
):
Expand Down
58 changes: 58 additions & 0 deletions backend/benefit/applications/tests/test_application_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,61 @@ def test_search_archival_application(handler_api_client, q, detected_pattern):
assert match["employee"]["last_name"] == searched_app.employee_last_name
assert match["company"]["business_id"] == searched_app.company.business_id
assert match["company"]["name"] == searched_app.company.name


@pytest.mark.parametrize(
"app_no",
[
(123456),
],
)
def test_search_related_applications(handler_api_client, app_no, application):
ImportArchivalApplicationsTestUtility.create_companies_for_archival_applications()
call_command("import_archival_applications", filename="test.xlsx", production=True)
archival_application = ArchivalApplication.objects.get(application_number="R001")
archival_application.refresh_from_db()

application.employee.first_name = str(archival_application.employee_first_name)
application.employee.last_name = str(archival_application.employee_last_name)
application.employee.social_security_number = "010192-9906"
application.employee.save()
application.application_number = app_no
application.status = ApplicationStatus.ACCEPTED
application.archived = True
application.save()
application.refresh_from_db()

params = urlencode(
{"q": "", "archived": 1, "archival": 1, "app_no": app_no},
)

response = handler_api_client.get(f"{api_url}?{params}")
data = response.json()
assert len(data["matches"]) == 2
for found_application in data["matches"]:
assert (
found_application["application_number"] == app_no
or archival_application.application_number
)
assert (
found_application["employee"]["first_name"]
== archival_application.employee_first_name
)
assert (
found_application["employee"]["last_name"]
== archival_application.employee_last_name
)

# Should find everyone with unspecified year_of_birth
archival_application.year_of_birth = "1900"
archival_application.save()
response = handler_api_client.get(f"{api_url}?{params}")
data = response.json()
assert len(data["matches"]) == 2

# Should not find anyone with wrong year_of_birth
archival_application.year_of_birth = "1991"
archival_application.save()
response = handler_api_client.get(f"{api_url}?{params}")
data = response.json()
assert len(data["matches"]) == 1
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def create_companies_for_archival_applications():
company.save()


def test_decision_proposal_drafting():
def test_import_archival_applications():
assert ArchivalApplication.objects.all().count() == 0
ImportArchivalApplicationsTestUtility.create_companies_for_archival_applications()

Expand Down
4 changes: 3 additions & 1 deletion frontend/benefit/handler/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@
"saveAndContinue": "Tallenna ja sulje",
"backToHandling": "Palauta käsittelyyn",
"handlingPanel": "Käsittelypaneeli",
"search": "Hae arkistosta",
"cancel": "Peruuta hakemus",
"addAttachment": "Liitä uusi tiedosto",
"addPreviouslyGrantedBenefit": "Lisää aikaisempi lisä",
Expand All @@ -1035,7 +1036,8 @@
"description": "Perustelu",
"acceptedSubsidy": "Myönnettävä tuki",
"eurosTotal": "{{total}} euroa yhteensä",
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}"
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}",
"searchPriorApplications": "Hae työllistettävän aiempia tukia"
},
"headings": {
"heading1": "Työnantajan tiedot",
Expand Down
4 changes: 3 additions & 1 deletion frontend/benefit/handler/public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@
"saveAndContinue": "Tallenna ja sulje",
"backToHandling": "Palauta käsittelyyn",
"handlingPanel": "Käsittelypaneeli",
"search": "Hae arkistosta",
"cancel": "Peruuta hakemus",
"addAttachment": "Liitä uusi tiedosto",
"addPreviouslyGrantedBenefit": "Lisää aikaisempi lisä",
Expand All @@ -1035,7 +1036,8 @@
"description": "Perustelu",
"acceptedSubsidy": "Myönnettävä tuki",
"eurosTotal": "{{total}} euroa yhteensä",
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}"
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}",
"searchPriorApplications": "Hae työllistettävän aiempia tukia"
},
"headings": {
"heading1": "Työnantajan tiedot",
Expand Down
4 changes: 3 additions & 1 deletion frontend/benefit/handler/public/locales/sv/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@
"saveAndContinue": "Tallenna ja sulje",
"backToHandling": "Palauta käsittelyyn",
"handlingPanel": "Käsittelypaneeli",
"search": "Hae arkistosta",
"cancel": "Peruuta hakemus",
"addAttachment": "Liitä uusi tiedosto",
"addPreviouslyGrantedBenefit": "Lisää aikaisempi lisä",
Expand All @@ -1035,7 +1036,8 @@
"description": "Perustelu",
"acceptedSubsidy": "Myönnettävä tuki",
"eurosTotal": "{{total}} euroa yhteensä",
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}"
"eurosPerMonth": "{{euros}} euroa kuukaudessa {{dateRange}}",
"searchPriorApplications": "Hae työllistettävän aiempia tukia"
},
"headings": {
"heading1": "Työnantajan tiedot",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import ReviewSection from 'benefit/handler/components/reviewSection/ReviewSectio
import { ACTIONLESS_STATUSES } from 'benefit/handler/constants';
import { ApplicationReviewViewProps } from 'benefit/handler/types/application';
import { ATTACHMENT_TYPES, ORGANIZATION_TYPES } from 'benefit-shared/constants';
import { Button, IconSearch } from 'hds-react';
import { useTranslation } from 'next-i18next';
import * as React from 'react';
import { $GridCell } from 'shared/components/forms/section/FormSection.sc';
import { getFullName } from 'shared/utils/application.utils';

import AttachmentsListView from '../../attachmentsListView/AttachmentsListView';

const openSearchPage = (id: string) => (): void => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(`/archive/?appNo=${id}`);
};

const EmployeeView: React.FC<ApplicationReviewViewProps> = ({ data }) => {
const translationsBase = 'common:review';
const { t } = useTranslation();
Expand Down Expand Up @@ -63,6 +69,17 @@ const EmployeeView: React.FC<ApplicationReviewViewProps> = ({ data }) => {
attachments={data.attachments || []}
/>
</$GridCell>

<$GridCell $colSpan={6} $colStart={1}>
<Button
onClick={openSearchPage(String(data.applicationNumber))}
theme="black"
variant="secondary"
iconLeft={<IconSearch />}
>
{t('common:review.actions.searchPriorApplications')}
</Button>
</$GridCell>
</ReviewSection>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { RadioButton, SearchInput, SelectionGroup } from 'hds-react';
import { ROUTES } from 'benefit/handler/constants';
import {
IconCross,
RadioButton,
SearchInput,
SelectionGroup,
StatusLabel,
} from 'hds-react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import * as React from 'react';
import Container from 'shared/components/container/Container';
import Heading from 'shared/components/forms/heading/Heading';
import {
$Grid,
$GridCell,
} from 'shared/components/forms/section/FormSection.sc';
import styled from 'styled-components';

import ApplicationArchiveList from './ApplicationArchiveList';
import { $Heading } from './ApplicationsArchive.sc';
Expand All @@ -16,9 +26,25 @@ import {
useApplicationsArchive,
} from './useApplicationsArchive';

const $SearchInputArea = styled.div`
max-width: 630px;
.custom-status-label {
font-size: 18px;
padding: 0.5em 1em;
margin-bottom: 2em;
display: flex;
align-items: center;
a {
display: flex;
align-items: center;
}
}
`;

const ApplicationsArchive: React.FC = () => {
const [searchString, setSearchString] = React.useState<string>('');

const [initialQuery, setInitialQuery] = React.useState<boolean>(true);
const [subsidyInEffect, setSubsidyInEffect] =
React.useState<SUBSIDY_IN_EFFECT | null>(
SUBSIDY_IN_EFFECT.RANGE_THREE_YEARS
Expand All @@ -30,25 +56,23 @@ const ApplicationsArchive: React.FC = () => {
FILTER_SELECTION.SUBSIDY_IN_EFFECT_RANGE_THREE_YEARS
);

const router = useRouter();
const applicationNum = router?.query?.appNo || null;
const { t, isSearchLoading, searchResults, submitSearch } =
useApplicationsArchive(
searchString,
true,
true,
subsidyInEffect,
decisionRange
decisionRange,
applicationNum ? applicationNum.toString() : null
);

const onSearch = (value: string): void => {
setSearchString(value);
submitSearch(value);
};

React.useEffect(() => {
submitSearch(searchString);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterSelection]);

const handleSubsidyFilterChange = (
selection: FILTER_SELECTION,
value?: SUBSIDY_IN_EFFECT
Expand All @@ -71,27 +95,61 @@ const ApplicationsArchive: React.FC = () => {
setFilterSelection(FILTER_SELECTION.NO_FILTER);
};

React.useEffect(() => {
if (!router || !router.isReady) return;
if (applicationNum && initialQuery) {
handleFiltersOff();
setInitialQuery(false);
} else if (!isSearchLoading) {
submitSearch(searchString);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterSelection, applicationNum, router, initialQuery]);

return (
<Container data-testid="application-list-archived">
<$Heading as="h1" data-testid="main-ingress">{`${t(
'common:header.navigation.archive'
)}`}</$Heading>
<>
<div style={{ maxWidth: 630 }}>
<SearchInput
helperText={t(
'common:search.fields.searchInput.keyword.helperText'
)}
label={t('common:search.fields.searchInput.keyword.label')}
placeholder={t(
'common:search.fields.searchInput.keyword.placeholder'
)}
onChange={(value) => setSearchString(value)}
onSubmit={(value) => onSearch(value)}
css="margin-bottom: var(--spacing-m);"
/>
</div>
<$SearchInputArea>
{!applicationNum && (
<SearchInput
helperText={t(
'common:search.fields.searchInput.keyword.helperText'
)}
label={t('common:search.fields.searchInput.keyword.label')}
placeholder={t(
'common:search.fields.searchInput.keyword.placeholder'
)}
onChange={(value) => setSearchString(value)}
onSubmit={(value) => onSearch(value)}
css="margin-bottom: var(--spacing-m);"
/>
)}

{applicationNum && (
<div>
<div
style={{
fontSize: '16px',
display: 'flex',
alignItems: 'center',
}}
>
<StatusLabel className="custom-status-label">
<div>
Haetaan aiempia työsuhteita hakemuksen{' '}
<u>{applicationNum}</u> perusteella
</div>
<Link href={ROUTES.APPLICATIONS_ARCHIVE}>
<IconCross />
</Link>
</StatusLabel>
</div>
</div>
)}
</$SearchInputArea>
<$Grid>
<$GridCell $colSpan={6}>
<Heading
Expand Down
Loading

0 comments on commit c0dc2e7

Please sign in to comment.