Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,38 @@ public class EligibilityCheckResource {

// By default, returns the most recent versions of all published checks owned by the calling user
// If the query parameter 'working' is set to true,
// then all the working check objects owned by the user are returned
// then the active (non-archived) working check objects owned by the user are returned
// Adding 'includeArchived=true' to a working request returns the archived ones alongside them,
// so a caller that renders both lists can do it with a single read.
// Published checks are never archived, so 'includeArchived=true' without 'working=true'
// is rejected rather than silently answered with an unfiltered published list
@GET
public Response getCustomChecks(
@Context SecurityIdentity identity,
@QueryParam("working") Boolean working
@QueryParam("working") Boolean working,
@QueryParam("includeArchived") Boolean includeArchived
) {
String userId = AuthUtils.getUserId(identity);
if (userId == null) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}

if (Boolean.TRUE.equals(includeArchived) && !Boolean.TRUE.equals(working)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "includeArchived requires working=true"))
.build();
}

List<EligibilityCheck> checks;

if (working != null && working){
Log.info("Fetching all working custom checks. User: " + userId);
checks = eligibilityCheckRepository.getWorkingCustomChecks(userId);
if (Boolean.TRUE.equals(working)){
if (Boolean.TRUE.equals(includeArchived)) {
Log.info("Fetching active and archived custom checks. User: " + userId);
checks = eligibilityCheckRepository.getAllWorkingCustomChecks(userId);
} else {
Log.info("Fetching active working custom checks. User: " + userId);
checks = eligibilityCheckRepository.getWorkingCustomChecks(userId);
}
} else {
Log.info("Fetching all published custom checks. User: " + userId);
checks = eligibilityCheckRepository.getLatestVersionPublishedCustomChecks(userId);
Expand Down Expand Up @@ -435,6 +451,43 @@ public Response archiveCustomCheck(@Context SecurityIdentity identity, @PathPara
}
}

@POST
@Path("/{checkId}/restore")
public Response restoreCustomCheck(@Context SecurityIdentity identity, @PathParam("checkId") String checkId) {
String userId = AuthUtils.getUserId(identity);
if (userId == null) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}

Optional<EligibilityCheck> checkOpt = eligibilityCheckRepository
.getWorkingCustomCheck(userId, checkId, true);
if (checkOpt.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND).build();
}

EligibilityCheck check = checkOpt.get();
if (!check.getOwnerId().equals(userId)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}

if (!check.getIsArchived()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Check is not archived"))
.build();
}

check.setIsArchived(false);
try {
eligibilityCheckRepository.updateWorkingCustomCheck(check);
return Response.ok(check, MediaType.APPLICATION_JSON).build();
} catch (Exception e) {
Log.error("Could not restore check " + checkId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Could not restore check"))
.build();
}
}

// ========== Sub-Resource Endpoints: Related Resources ==========

/* Endpoint for returning all Published Check Versions related to a given Working Eligibility Check */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public interface EligibilityCheckRepository {

List<EligibilityCheck> getWorkingCustomChecks(String userId);

List<EligibilityCheck> getAllWorkingCustomChecks(String userId);

List<EligibilityCheck> getPublishedCheckVersions(EligibilityCheck workingCustomCheck) throws Exception;

List<EligibilityCheck> getLatestVersionPublishedCustomChecks(String userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ public class EligibilityCheckRepositoryImpl implements EligibilityCheckRepositor
private StorageService storageService;

public List<EligibilityCheck> getWorkingCustomChecks(String userId){
return getAllWorkingCustomChecks(userId).stream()
.filter(check -> !check.getIsArchived())
.toList();
}

public List<EligibilityCheck> getAllWorkingCustomChecks(String userId){
List<Map<String, Object>> checkMaps = FirestoreUtils.getFirestoreDocsByField(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, FieldNames.OWNER_ID, userId);
ObjectMapper mapper = new ObjectMapper();
return checkMaps.stream()
.map(checkMap -> mapper.convertValue(checkMap, EligibilityCheck.class))
.filter(check -> !check.getIsArchived())
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
Expand Down Expand Up @@ -226,6 +227,79 @@ private CreateCheckRequest createCheckRequest() {
List.of());
}

@Test
void getCustomChecksCanIncludeArchivedChecksInOneRead() {
EligibilityCheck archivedCheck = new EligibilityCheck(
"old-check", "income", "an old check", List.of(), USER_ID);
archivedCheck.setIsArchived(true);
when(repository.getAllWorkingCustomChecks(USER_ID))
.thenReturn(List.of(workingCheck, archivedCheck));

Response response = resource.getCustomChecks(identity, true, true);

assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
assertEquals(List.of(workingCheck, archivedCheck), response.getEntity());
verify(repository).getAllWorkingCustomChecks(USER_ID);
verify(repository, never()).getWorkingCustomChecks(USER_ID);
}

@Test
void getCustomChecksOmitsArchivedChecksByDefault() {
when(repository.getWorkingCustomChecks(USER_ID)).thenReturn(List.of(workingCheck));

Response response = resource.getCustomChecks(identity, true, null);

assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
assertEquals(List.of(workingCheck), response.getEntity());
verify(repository).getWorkingCustomChecks(USER_ID);
verify(repository, never()).getAllWorkingCustomChecks(USER_ID);
}

@Test
void getCustomChecksRejectsIncludeArchivedWithoutWorking() {
Response response = resource.getCustomChecks(identity, null, true);

assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
assertEquals("includeArchived requires working=true",
((java.util.Map<?, ?>) response.getEntity()).get("error"));
verify(repository, never()).getLatestVersionPublishedCustomChecks(USER_ID);
}

@Test
void restoreCustomCheckMakesAnArchivedCheckActive() throws Exception {
workingCheck.setIsArchived(true);
when(repository.getWorkingCustomCheck(USER_ID, CHECK_ID, true))
.thenReturn(Optional.of(workingCheck));

Response response = resource.restoreCustomCheck(identity, CHECK_ID);

assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
assertFalse(workingCheck.getIsArchived());
verify(repository).updateWorkingCustomCheck(workingCheck);
}

@Test
void restoreCustomCheckRejectsAnActiveCheck() throws Exception {
when(repository.getWorkingCustomCheck(USER_ID, CHECK_ID, true))
.thenReturn(Optional.of(workingCheck));

Response response = resource.restoreCustomCheck(identity, CHECK_ID);

assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
assertEquals("Check is not archived", ((java.util.Map<?, ?>) response.getEntity()).get("error"));
verify(repository, never()).updateWorkingCustomCheck(any());
}

@Test
void restoreCustomCheckReturnsNotFoundForAnUnknownCheck() {
when(repository.getWorkingCustomCheck(USER_ID, CHECK_ID, true))
.thenReturn(Optional.empty());

Response response = resource.restoreCustomCheck(identity, CHECK_ID);

assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
}

@Test
void firstPublishKeepsInitialVersion() throws Exception {
when(repository.getPublishedCheckVersions(workingCheck)).thenReturn(List.of());
Expand Down
31 changes: 27 additions & 4 deletions builder-frontend/src/api/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,20 @@ export const validateCheckDmn = async (
}
};

export const fetchUserDefinedChecks = async (
working: boolean,
): Promise<EligibilityCheck[]> => {
export const fetchUserDefinedChecks = async ({
working,
includeArchived = false,
}: {
working: boolean;
includeArchived?: boolean;
}): Promise<EligibilityCheck[]> => {
const workingQueryParam = working ? "true" : "false";
let url: string = apiUrl + `/custom-checks?working=${workingQueryParam}`;
const includeArchivedQueryParam = includeArchived
? "&includeArchived=true"
: "";
const url =
apiUrl +
`/custom-checks?working=${workingQueryParam}${includeArchivedQueryParam}`;

try {
const response = await authGet(url);
Expand Down Expand Up @@ -243,3 +252,17 @@ export const archiveCheck = async (checkId: string): Promise<void> => {
throw error;
}
};

export const restoreCheck = async (checkId: string): Promise<void> => {
const url = apiUrl + `/custom-checks/${checkId}/restore`;
try {
const response = await authPost(url);

if (!response.ok) {
throw new Error(`Restore failed with status: ${response.status}`);
}
} catch (error) {
console.error("Error restoring check:", error);
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSignal, For, Setter, Show } from "solid-js";
import { Accessor, createSignal, For, Setter, Show } from "solid-js";
import { useNavigate } from "@solidjs/router";

import Loading from "@/components/Loading";
Expand All @@ -13,11 +13,18 @@ import { ArchiveCheck } from "@/components/homeScreen/eligibilityCheckList/modal
import { Button } from "@/components/shared/Button";

const EligibilityChecksList = () => {
const { checks, actions, actionInProgress, initialLoadStatus } =
eligibilityCheckResource();
const {
checks,
archivedChecks,
actions,
actionInProgress,
initialLoadStatus,
} = eligibilityCheckResource();
const navigate = useNavigate();

const [addingNewCheck, setAddingNewCheck] = createSignal<boolean>(false);
const [showArchivedChecks, setShowArchivedChecks] =
createSignal<boolean>(false);

const [checkIdToRemove, setCheckIdToRemove] = createSignal<null | string>(
null,
Expand Down Expand Up @@ -77,6 +84,41 @@ const EligibilityChecksList = () => {
)}
</For>
</div>
<Show when={archivedChecks().length > 0}>
<div class="mt-8 border-t border-gray-300 pt-4">
<Button
variant="outline-secondary"
aria-expanded={showArchivedChecks()}
aria-controls="archived-checks"
onClick={() => setShowArchivedChecks((shown) => !shown)}
>
{showArchivedChecks() ? "Hide" : "Show"} archived checks (
{archivedChecks().length})
</Button>
<Show when={showArchivedChecks()}>
<section id="archived-checks" class="mt-4">
<h2 class="text-xl font-bold mb-1">Archived checks</h2>
<p class="mb-4 text-gray-700">
Restore a check to edit it or use its name again.
</p>
<div class="grid gap-4 justify-items-center grid-cols-1 md:grid-cols-2 xl:grid-cols-3">
<For each={archivedChecks()}>
{(check) => (
<CheckCard
eligibilityCheck={check}
navigateToCheck={navigateToCheck}
setCheckIdToRemove={setCheckIdToRemove}
archived
onRestore={() => actions.restoreCheck(check.id)}
actionInProgress={actionInProgress}
/>
)}
</For>
</div>
</section>
</Show>
</div>
</Show>
<Modal
show={checkIdToRemove() !== null}
onClose={() => setCheckIdToRemove(null)}
Expand All @@ -98,10 +140,16 @@ const CheckCard = ({
eligibilityCheck,
navigateToCheck,
setCheckIdToRemove,
archived = false,
onRestore,
actionInProgress,
}: {
eligibilityCheck: EligibilityCheck;
navigateToCheck: (check: EligibilityCheck) => void;
setCheckIdToRemove: Setter<string>;
archived?: boolean;
onRestore?: () => Promise<void>;
actionInProgress?: Accessor<boolean>;
}) => {
return (
<div class="w-full flex">
Expand All @@ -124,22 +172,36 @@ const CheckCard = ({
id={"benefit-card-actions-" + eligibilityCheck.id}
class="p-4 flex justify-end space-x-2"
>
<Button
variant="outline-secondary"
onClick={() => {
navigateToCheck(eligibilityCheck);
}}
>
Edit
</Button>
<Button
variant="outline-danger"
onClick={() => {
setCheckIdToRemove(eligibilityCheck.id);
}}
<Show
when={archived}
fallback={
<>
<Button
variant="outline-secondary"
onClick={() => {
navigateToCheck(eligibilityCheck);
}}
>
Edit
</Button>
<Button
variant="outline-danger"
onClick={() => {
setCheckIdToRemove(eligibilityCheck.id);
}}
>
Archive
</Button>
</>
}
>
Archive
</Button>
<Button
disabled={actionInProgress?.()}
onClick={() => void onRestore?.()}
>
Restore
</Button>
</Show>
</div>
</div>
</div>
Expand Down
Loading
Loading