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

PSP-7886 : Prevent users from adding new takes, and/or deleting completed takes from a non-active (e.g. draft or active) acquisition file #3923

Merged
merged 5 commits into from
Apr 4, 2024
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
30 changes: 27 additions & 3 deletions source/backend/api/Services/TakeService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
using Pims.Api.Constants;
using Pims.Api.Models.CodeTypes;
using Pims.Core.Exceptions;
using Pims.Dal.Entities;
Expand Down Expand Up @@ -61,11 +61,35 @@ public IEnumerable<PimsTake> UpdateAcquisitionPropertyTakes(long acquisitionFile
_user.ThrowIfNotAuthorized(Permissions.PropertyView, Permissions.AcquisitionFileView);

var currentAcquistionFile = _acqFileRepository.GetByAcquisitionFilePropertyId(acquisitionFilePropertyId);
var currentFilePropertyTakes = _takeRepository.GetAllByPropertyAcquisitionFileId(acquisitionFilePropertyId);

var currentAcquisitionStatus = Enum.Parse<AcquisitionStatusTypes>(currentAcquistionFile.AcquisitionFileStatusTypeCode);
if (!_statusSolver.CanEditTakes(currentAcquisitionStatus) && !_user.HasPermission(Permissions.SystemAdmin))

// No user can update the takes when the File is not Active/Draft
if (!_statusSolver.CanEditTakes(currentAcquisitionStatus))
{
throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion.");
}
else
{
throw new BusinessRuleViolationException("The file you are editing is not active or draft, so you cannot save changes. Refresh your browser to see file state.");
// Complete Takes can only be deleted or set to InProgress by Admins when File is Active/Draft
if (!_user.HasPermission(Permissions.SystemAdmin))
{
var currentCompleteTakes = currentFilePropertyTakes
.Where(x => x.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString()).ToList();

if (currentCompleteTakes.Count > 0)
{
foreach (var completeTake in currentCompleteTakes)
{
var updatedTake = takes.FirstOrDefault(x => x.TakeId == completeTake.TakeId);
if (updatedTake is null || updatedTake?.TakeStatusTypeCode != completeTake.TakeStatusTypeCode)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure about this logic, as it would seem to allow a completed take to be updated as long as its status type is not modified.

given that the frontend prevents editing completed takes, I think we can just ignore any take modifications to completed takes here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

let me double check 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.

Updated frontend

{
throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion.");
}
}
}
}
}

_takeRepository.UpdateAcquisitionPropertyTakes(acquisitionFilePropertyId, takes);
Expand Down
61 changes: 59 additions & 2 deletions source/backend/tests/unit/api/Services/TakeServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public void Update_NoPermission()
}

[Fact]
public void Update_InvalidStatus()
public void Update_InvalidStatus_AcquisitionFile_Complete()
{
// Arrange
var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView);
Expand All @@ -175,7 +175,64 @@ public void Update_InvalidStatus()
Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List<PimsTake>());

// Assert
act.Should().Throw<BusinessRuleViolationException>();
act.Should().Throw<BusinessRuleViolationException>().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion.");
}

[Fact]
public void Update_InvalidStatus_AcquisitionFile_Active_DeleteCompleteTake_NotAdmin()
{
// Arrange
var service = this.CreateWithPermissions(Permissions.PropertyView, Permissions.AcquisitionFileView);

var acqRepository = this._helper.GetService<Mock<IAcquisitionFileRepository>>();
acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny<long>())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() });

PimsTake completedTake = new()
{
TakeId = 100,
TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(),
};

var takeRepository = this._helper.GetService<Mock<ITakeRepository>>();
takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny<long>())).Returns(new List<PimsTake>() { completedTake });

var solver = this._helper.GetService<Mock<IAcquisitionStatusSolver>>();
solver.Setup(x => x.CanEditTakes(It.IsAny<AcquisitionStatusTypes?>())).Returns(true);

// Act
Action act = () => service.UpdateAcquisitionPropertyTakes(1, new List<PimsTake>());

// Assert
act.Should().Throw<BusinessRuleViolationException>().WithMessage("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion.");
}

[Fact]
public void Update_AcquisitionFile_Active_DeleteCompleteTake_Admin_Success()
{
// Arrange
var service = this.CreateWithPermissions(Permissions.SystemAdmin, Permissions.PropertyView, Permissions.AcquisitionFileView);

var acqRepository = this._helper.GetService<Mock<IAcquisitionFileRepository>>();
acqRepository.Setup(x => x.GetByAcquisitionFilePropertyId(It.IsAny<long>())).Returns(new PimsAcquisitionFile() { AcquisitionFileStatusTypeCode = AcquisitionStatusTypes.ACTIVE.ToString() });

PimsTake completedTake = new()
{
TakeId = 100,
TakeStatusTypeCode = AcquisitionTakeStatusTypes.COMPLETE.ToString(),
};

var takeRepository = this._helper.GetService<Mock<ITakeRepository>>();
takeRepository.Setup(x => x.GetAllByPropertyAcquisitionFileId(It.IsAny<long>())).Returns(new List<PimsTake>() { completedTake });

var solver = this._helper.GetService<Mock<IAcquisitionStatusSolver>>();
solver.Setup(x => x.CanEditTakes(It.IsAny<AcquisitionStatusTypes?>())).Returns(true);

// Act
var result = service.UpdateAcquisitionPropertyTakes(1, new List<PimsTake>());

// Assert
Assert.NotNull(result);
takeRepository.Verify(x => x.UpdateAcquisitionPropertyTakes(1, new List<PimsTake>()), Times.Once);
}
}
}
9 changes: 0 additions & 9 deletions source/frontend/src/constants/acquisitionFileStatus.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AcquisitionStatus } from '@/constants/acquisitionFileStatus';
import { Claims, Roles } from '@/constants/index';
import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock';
import { toTypeCode } from '@/utils/formUtils';
import { act, render, RenderOptions, userEvent } from '@/utils/test-utils';

import AcquisitionMenu, { IAcquisitionMenuProps } from './AcquisitionMenu';
import { ApiGen_CodeTypes_AcquisitionStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionStatusTypes';

// mock auth library
jest.mock('@react-keycloak/web');
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('AcquisitionMenu component', () => {
{
acquisitionFile: {
...mockAcquisitionFileResponse(),
fileStatusTypeCode: toTypeCode(AcquisitionStatus.Complete),
fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_AcquisitionStatusTypes.COMPLT),
},
items: testData,
selectedIndex: 1,
Expand All @@ -148,7 +148,7 @@ describe('AcquisitionMenu component', () => {
{
acquisitionFile: {
...mockAcquisitionFileResponse(),
fileStatusTypeCode: toTypeCode(AcquisitionStatus.Complete),
fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_AcquisitionStatusTypes.COMPLT),
},
items: testData,
selectedIndex: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createMemoryHistory } from 'history';

import { AcquisitionStatus } from '@/constants/acquisitionFileStatus';
import Claims from '@/constants/claims';
import { Roles } from '@/constants/index';
import { mockAcquisitionFileResponse } from '@/mocks/acquisitionFiles.mock';
Expand All @@ -15,6 +14,7 @@ import { act, render, RenderOptions, userEvent, waitFor } from '@/utils/test-uti
import CompensationRequisitionDetailView, {
CompensationRequisitionDetailViewProps,
} from './CompensationRequisitionDetailView';
import { ApiGen_CodeTypes_AcquisitionStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionStatusTypes';

const setEditMode = jest.fn();

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('Compensation Detail View Component', () => {
props: {
acquisitionFile: {
...acquistionFile,
fileStatusTypeCode: toTypeCodeNullable(AcquisitionStatus.Active),
fileStatusTypeCode: toTypeCodeNullable(ApiGen_CodeTypes_AcquisitionStatusTypes.ACTIVE),
},
compensation: { ...mockFinalCompensation, isDraft: true },
},
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('Compensation Detail View Component', () => {
props: {
acquisitionFile: {
...acquistionFile,
fileStatusTypeCode: toTypeCodeNullable(AcquisitionStatus.Complete),
fileStatusTypeCode: toTypeCodeNullable(ApiGen_CodeTypes_AcquisitionStatusTypes.COMPLT),
},
compensation: { ...mockFinalCompensation, isDraft: false },
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createMemoryHistory } from 'history';

import { AcquisitionStatus } from '@/constants/acquisitionFileStatus';
import Claims from '@/constants/claims';
import Roles from '@/constants/roles';
import {
Expand All @@ -15,6 +14,7 @@ import { toTypeCode, toTypeCodeNullable } from '@/utils/formUtils';
import { act, render, RenderOptions, userEvent, waitFor } from '@/utils/test-utils';

import CompensationListView, { ICompensationListViewProps } from './CompensationListView';
import { ApiGen_CodeTypes_AcquisitionStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionStatusTypes';

const storeState = {
[lookupCodesSlice.name]: { lookupCodes: mockLookups },
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('compensation list view', () => {
const { findAllByTitle } = setup({
acquisitionFile: {
...mockAcquisitionFileResponse(),
fileStatusTypeCode: toTypeCodeNullable(AcquisitionStatus.Active),
fileStatusTypeCode: toTypeCodeNullable(ApiGen_CodeTypes_AcquisitionStatusTypes.ACTIVE),
},
compensations: compensations,
claims: [Claims.COMPENSATION_REQUISITION_DELETE],
Expand All @@ -136,7 +136,7 @@ describe('compensation list view', () => {
const { queryByTestId } = setup({
acquisitionFile: {
...mockAcquisitionFileResponse(),
fileStatusTypeCode: toTypeCode(AcquisitionStatus.Active),
fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_AcquisitionStatusTypes.ACTIVE),
},
compensations: compensations,
claims: [Claims.COMPENSATION_REQUISITION_DELETE],
Expand All @@ -151,7 +151,7 @@ describe('compensation list view', () => {
const { queryByTestId } = setup({
acquisitionFile: {
...mockAcquisitionFileResponse(),
fileStatusTypeCode: toTypeCode(AcquisitionStatus.Active),
fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_AcquisitionStatusTypes.ACTIVE),
},
compensations: compensations,
claims: [Claims.COMPENSATION_REQUISITION_DELETE],
Expand Down
Loading
Loading