diff --git a/client/packages/common/src/intl/locales/en/common.json b/client/packages/common/src/intl/locales/en/common.json index 4dafb56594..e92905f80e 100644 --- a/client/packages/common/src/intl/locales/en/common.json +++ b/client/packages/common/src/intl/locales/en/common.json @@ -1468,6 +1468,10 @@ "messages.zero-line-quantities_one": "The quantity of 1 line has been set to 0", "messages.zero-line-quantities_other": "The quantity of {{count}} lines has been set to 0", "messages.zero-return-quantity-will-delete-lines": "There are no return quantities specified. Click OK again to confirm and remove these lines from the return.", + "messages.cannot-delete-finalised-requisition": "Cannot delete finalised requisition", + "messages.record-not-found": "Record not found", + "messages.cannot-delete-requisition-with-shipment": "Cannot delete requisition linked to a shipment", + "messages.cannot-delete-transfer-requisition": "Cannot delete transfer requisition", "monitoring": "Monitoring", "multiple": "[multiple]", "outbound-shipment": "Outbound Shipments", diff --git a/client/packages/common/src/types/schema.ts b/client/packages/common/src/types/schema.ts index b13b063d9f..0c5a879ba4 100644 --- a/client/packages/common/src/types/schema.ts +++ b/client/packages/common/src/types/schema.ts @@ -767,11 +767,13 @@ export type BatchRequestRequisitionResponse = { export type BatchResponseRequisitionInput = { continueOnError?: InputMaybe; deleteResponseRequisitionLines?: InputMaybe>; + deleteResponseRequisitions?: InputMaybe>; }; export type BatchResponseRequisitionResponse = { __typename: 'BatchResponseRequisitionResponse'; deleteResponseRequisitionLines?: Maybe>; + deleteResponseRequisitions?: Maybe>; }; export type BatchStocktakeInput = { @@ -1588,6 +1590,19 @@ export type DeleteResponse = { id: Scalars['String']['output']; }; +export type DeleteResponseRequisitionError = { + __typename: 'DeleteResponseRequisitionError'; + error: DeleteResponseRequisitionErrorInterface; +}; + +export type DeleteResponseRequisitionErrorInterface = { + description: Scalars['String']['output']; +}; + +export type DeleteResponseRequisitionInput = { + id: Scalars['String']['input']; +}; + export type DeleteResponseRequisitionLineError = { __typename: 'DeleteResponseRequisitionLineError'; error: DeleteResponseRequisitionLineErrorInterface; @@ -1609,6 +1624,14 @@ export type DeleteResponseRequisitionLineResponseWithId = { response: DeleteResponseRequisitionLineResponse; }; +export type DeleteResponseRequisitionResponse = DeleteResponse | DeleteResponseRequisitionError; + +export type DeleteResponseRequisitionResponseWithId = { + __typename: 'DeleteResponseRequisitionResponseWithId'; + id: Scalars['String']['output']; + response: DeleteResponseRequisitionResponse; +}; + export type DeleteStocktakeError = { __typename: 'DeleteStocktakeError'; error: DeleteStocktakeErrorInterface; @@ -2209,6 +2232,11 @@ export type FinaliseRnRFormInput = { export type FinaliseRnRFormResponse = RnRFormNode; +export type FinalisedRequisition = DeleteResponseRequisitionErrorInterface & { + __typename: 'FinalisedRequisition'; + description: Scalars['String']['output']; +}; + export enum ForeignKey { InvoiceId = 'invoiceId', ItemId = 'itemId', @@ -3703,6 +3731,11 @@ export type LedgerSortInput = { key: LedgerSortFieldInput; }; +export type LineDeleteError = DeleteResponseRequisitionErrorInterface & { + __typename: 'LineDeleteError'; + description: Scalars['String']['output']; +}; + export type LinkPatientPatientToStoreError = { __typename: 'LinkPatientPatientToStoreError'; error: LinkPatientPatientToStoreErrorInterface; @@ -3947,6 +3980,7 @@ export type Mutations = { deletePrescriptionLine: DeletePrescriptionLineResponse; deleteRequestRequisition: DeleteRequestRequisitionResponse; deleteRequestRequisitionLine: DeleteRequestRequisitionLineResponse; + deleteResponseRequisition: DeleteResponseRequisitionResponse; deleteResponseRequisitionLine: DeleteResponseRequisitionLineResponse; deleteStocktake: DeleteStocktakeResponse; deleteStocktakeLine: DeleteStocktakeLineResponse; @@ -4215,6 +4249,12 @@ export type MutationsDeleteRequestRequisitionLineArgs = { }; +export type MutationsDeleteResponseRequisitionArgs = { + input: DeleteResponseRequisitionInput; + storeId: Scalars['String']['input']; +}; + + export type MutationsDeleteResponseRequisitionLineArgs = { input: DeleteResponseRequisitionLineInput; storeId: Scalars['String']['input']; @@ -6285,7 +6325,7 @@ export type RecordBelongsToAnotherStore = DeleteAssetErrorInterface & DeleteAsse description: Scalars['String']['output']; }; -export type RecordNotFound = AddFromMasterListErrorInterface & AddToInboundShipmentFromMasterListErrorInterface & AddToOutboundShipmentFromMasterListErrorInterface & AllocateOutboundShipmentUnallocatedLineErrorInterface & CreateRequisitionShipmentErrorInterface & DeleteAssetCatalogueItemErrorInterface & DeleteAssetErrorInterface & DeleteAssetLogReasonErrorInterface & DeleteCustomerReturnErrorInterface & DeleteErrorInterface & DeleteInboundShipmentErrorInterface & DeleteInboundShipmentLineErrorInterface & DeleteInboundShipmentServiceLineErrorInterface & DeleteLocationErrorInterface & DeleteOutboundShipmentLineErrorInterface & DeleteOutboundShipmentServiceLineErrorInterface & DeleteOutboundShipmentUnallocatedLineErrorInterface & DeletePrescriptionErrorInterface & DeletePrescriptionLineErrorInterface & DeleteRequestRequisitionErrorInterface & DeleteRequestRequisitionLineErrorInterface & DeleteResponseRequisitionLineErrorInterface & DeleteSupplierReturnErrorInterface & DeleteVaccineCourseErrorInterface & NodeErrorInterface & RequisitionLineChartErrorInterface & RequisitionLineStatsErrorInterface & SupplyRequestedQuantityErrorInterface & UpdateAssetErrorInterface & UpdateErrorInterface & UpdateInboundShipmentErrorInterface & UpdateInboundShipmentLineErrorInterface & UpdateInboundShipmentServiceLineErrorInterface & UpdateLocationErrorInterface & UpdateNameErrorInterface & UpdateNamePropertiesErrorInterface & UpdateOutboundShipmentLineErrorInterface & UpdateOutboundShipmentServiceLineErrorInterface & UpdateOutboundShipmentUnallocatedLineErrorInterface & UpdatePrescriptionErrorInterface & UpdatePrescriptionLineErrorInterface & UpdateRequestRequisitionErrorInterface & UpdateRequestRequisitionLineErrorInterface & UpdateResponseRequisitionErrorInterface & UpdateResponseRequisitionLineErrorInterface & UpdateReturnOtherPartyErrorInterface & UpdateSensorErrorInterface & UpdateStockLineErrorInterface & UseSuggestedQuantityErrorInterface & { +export type RecordNotFound = AddFromMasterListErrorInterface & AddToInboundShipmentFromMasterListErrorInterface & AddToOutboundShipmentFromMasterListErrorInterface & AllocateOutboundShipmentUnallocatedLineErrorInterface & CreateRequisitionShipmentErrorInterface & DeleteAssetCatalogueItemErrorInterface & DeleteAssetErrorInterface & DeleteAssetLogReasonErrorInterface & DeleteCustomerReturnErrorInterface & DeleteErrorInterface & DeleteInboundShipmentErrorInterface & DeleteInboundShipmentLineErrorInterface & DeleteInboundShipmentServiceLineErrorInterface & DeleteLocationErrorInterface & DeleteOutboundShipmentLineErrorInterface & DeleteOutboundShipmentServiceLineErrorInterface & DeleteOutboundShipmentUnallocatedLineErrorInterface & DeletePrescriptionErrorInterface & DeletePrescriptionLineErrorInterface & DeleteRequestRequisitionErrorInterface & DeleteRequestRequisitionLineErrorInterface & DeleteResponseRequisitionErrorInterface & DeleteResponseRequisitionLineErrorInterface & DeleteSupplierReturnErrorInterface & DeleteVaccineCourseErrorInterface & NodeErrorInterface & RequisitionLineChartErrorInterface & RequisitionLineStatsErrorInterface & SupplyRequestedQuantityErrorInterface & UpdateAssetErrorInterface & UpdateErrorInterface & UpdateInboundShipmentErrorInterface & UpdateInboundShipmentLineErrorInterface & UpdateInboundShipmentServiceLineErrorInterface & UpdateLocationErrorInterface & UpdateNameErrorInterface & UpdateNamePropertiesErrorInterface & UpdateOutboundShipmentLineErrorInterface & UpdateOutboundShipmentServiceLineErrorInterface & UpdateOutboundShipmentUnallocatedLineErrorInterface & UpdatePrescriptionErrorInterface & UpdatePrescriptionLineErrorInterface & UpdateRequestRequisitionErrorInterface & UpdateRequestRequisitionLineErrorInterface & UpdateResponseRequisitionErrorInterface & UpdateResponseRequisitionLineErrorInterface & UpdateReturnOtherPartyErrorInterface & UpdateSensorErrorInterface & UpdateStockLineErrorInterface & UseSuggestedQuantityErrorInterface & { __typename: 'RecordNotFound'; description: Scalars['String']['output']; }; @@ -6648,6 +6688,11 @@ export type RequisitionSortInput = { key: RequisitionSortFieldInput; }; +export type RequisitionWithShipment = DeleteResponseRequisitionErrorInterface & { + __typename: 'RequisitionWithShipment'; + description: Scalars['String']['output']; +}; + export type RequisitionsResponse = RequisitionConnector; export type ResponseRequisitionCounts = { @@ -7497,6 +7542,11 @@ export type TokenExpired = RefreshTokenErrorInterface & { description: Scalars['String']['output']; }; +export type TransferredRequisition = DeleteResponseRequisitionErrorInterface & { + __typename: 'TransferredRequisition'; + description: Scalars['String']['output']; +}; + export type UnallocatedLineForItemAlreadyExists = InsertOutboundShipmentUnallocatedLineErrorInterface & { __typename: 'UnallocatedLineForItemAlreadyExists'; description: Scalars['String']['output']; diff --git a/client/packages/requisitions/src/ResponseRequisition/ListView/ListView.tsx b/client/packages/requisitions/src/ResponseRequisition/ListView/ListView.tsx index ea73b8a03f..622e13aaf3 100644 --- a/client/packages/requisitions/src/ResponseRequisition/ListView/ListView.tsx +++ b/client/packages/requisitions/src/ResponseRequisition/ListView/ListView.tsx @@ -131,7 +131,10 @@ export const ResponseRequisitionListView: FC = () => { t(getApprovalStatusKey(rowData.approvalStatus)), }); } - columnDefinitions.push(['comment', { minWidth: 400, Cell: TooltipTextCell }]); + columnDefinitions.push( + ['comment', { minWidth: 350, Cell: TooltipTextCell }], + ['selection'] + ); const columns = useColumns( columnDefinitions, diff --git a/client/packages/requisitions/src/ResponseRequisition/ListView/Toolbar.tsx b/client/packages/requisitions/src/ResponseRequisition/ListView/Toolbar.tsx index 639526f734..8a1de73ea6 100644 --- a/client/packages/requisitions/src/ResponseRequisition/ListView/Toolbar.tsx +++ b/client/packages/requisitions/src/ResponseRequisition/ListView/Toolbar.tsx @@ -6,11 +6,16 @@ import { Box, useTranslation, RequisitionNodeStatus, + DeleteIcon, + DropdownMenu, + DropdownMenuItem, } from '@openmsupply-client/common'; +import { useResponse } from '../api'; export const Toolbar: FC<{ filter: FilterController; }> = () => { + const onDelete = useResponse.document.deleteSelected(); const t = useTranslation(); return ( @@ -51,6 +56,11 @@ export const Toolbar: FC<{ ]} /> + + + {t('button.delete-lines')} + + ); }; diff --git a/client/packages/requisitions/src/ResponseRequisition/api/api.ts b/client/packages/requisitions/src/ResponseRequisition/api/api.ts index b348396a42..5fcc422954 100644 --- a/client/packages/requisitions/src/ResponseRequisition/api/api.ts +++ b/client/packages/requisitions/src/ResponseRequisition/api/api.ts @@ -82,6 +82,9 @@ const responseParser = { status: responseParser.toStatus(requisition), }; }, + toDelete: (line: ResponseFragment) => { + return { id: line.id }; + }, toDeleteLine: (line: ResponseLineFragment) => ({ id: line.id }), toUpdateLine: ( patch: DraftResponseLine @@ -223,6 +226,23 @@ export const getResponseQueries = (sdk: Sdk, storeId: string) => ({ throw new Error('Unable to update requisition'); }, + deleteResponses: async (requisitions: ResponseFragment[]) => { + const deleteResponseRequisitions = requisitions.map( + responseParser.toDelete + ); + const result = await sdk.deleteRequest({ + storeId, + input: { deleteResponseRequisitions }, + }); + + const { batchResponseRequisition } = result || {}; + + if (batchResponseRequisition?.deleteResponseRequisitions) { + return batchResponseRequisition.deleteResponseRequisitions; + } + + throw new Error('Could not delete requisitions'); + }, deleteLines: async (responseLines: ResponseLineFragment[]) => { const ids = responseLines.map(responseParser.toDeleteLine); const result = await sdk.deleteResponseLines({ ids, storeId }); diff --git a/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/index.ts b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/index.ts index 8879b8bf4e..0467735f6c 100644 --- a/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/index.ts +++ b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/index.ts @@ -5,6 +5,8 @@ import { useResponseFields } from './useResponseFields'; import { useResponses } from './useResponses'; import { useResponsesAll } from './useResponsesAll'; import { useUpdateResponse } from './useUpdateResponse'; +import { useDeleteResponses } from './useDeleteResponses'; +import { useDeleteSelectedResponseRequisitions } from './useDeleteSelectedResponseRequisitions'; export const Document = { useResponse, @@ -12,6 +14,8 @@ export const Document = { useResponses, useResponsesAll, useUpdateResponse, + useDeleteResponses, + useDeleteSelectedResponseRequisitions, useInsertResponse, useInsertProgramResponse, }; diff --git a/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteResponses.ts b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteResponses.ts new file mode 100644 index 0000000000..ec5a734aca --- /dev/null +++ b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteResponses.ts @@ -0,0 +1,12 @@ +import { useQueryClient, useMutation } from '@openmsupply-client/common'; +import { useResponseApi } from '../utils/useResponseApi'; + +export const useDeleteResponses = () => { + const queryClient = useQueryClient(); + const api = useResponseApi(); + return useMutation(api.deleteResponses, { + onSuccess: () => { + queryClient.invalidateQueries(api.keys.base()); + }, + }); +}; diff --git a/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteSelectedResponseRequisitions.ts b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteSelectedResponseRequisitions.ts new file mode 100644 index 0000000000..a959a34504 --- /dev/null +++ b/client/packages/requisitions/src/ResponseRequisition/api/hooks/document/useDeleteSelectedResponseRequisitions.ts @@ -0,0 +1,63 @@ +import { + useTranslation, + useTableStore, + RequisitionNodeStatus, + useDeleteConfirmation, + useUrlQueryParams, +} from '@openmsupply-client/common'; +import { ResponseFragment } from '../../operations.generated'; +import { useDeleteResponses } from './useDeleteResponses'; +import { useResponses } from './useResponses'; + +export const useDeleteSelectedResponseRequisitions = () => { + const { queryParams } = useUrlQueryParams({ + initialSort: { key: 'createdDatetime', dir: 'desc' }, + }); + const { data: rows } = useResponses(queryParams); + const { mutateAsync } = useDeleteResponses(); + const t = useTranslation(); + const { selectedRows } = useTableStore(state => ({ + selectedRows: Object.keys(state.rowState) + .filter(id => state.rowState[id]?.isSelected) + .map(selectedId => rows?.nodes?.find(({ id }) => selectedId === id)) + .filter(Boolean) as ResponseFragment[], + })); + const deleteAction = async () => { + let result = await mutateAsync(selectedRows).catch(err => { + throw err; + }); + // check for errors + result.forEach(line => { + if (line.response.__typename == 'DeleteResponseRequisitionError') { + switch (line.response.error.__typename) { + case 'FinalisedRequisition': + throw Error(t('messages.cannot-delete-finalised-requisition')); + case 'RecordNotFound': + throw Error(t('messages.record-not-found')); + case 'RequisitionWithShipment': + throw Error(t('messages.cannot-delete-requisition-with-shipment')); + case 'TransferredRequisition': + throw Error(t('messages.cannot-delete-transfer-requisition')); + } + } + }); + }; + + const confirmAndDelete = useDeleteConfirmation({ + selectedRows, + deleteAction, + canDelete: selectedRows.every( + ({ status }) => status !== RequisitionNodeStatus.Finalised + ), + messages: { + confirmMessage: t('messages.confirm-delete-requisitions', { + count: selectedRows.length, + }), + deleteSuccess: t('messages.deleted-orders', { + count: selectedRows.length, + }), + cantDelete: (err: Error) => err.message, + }, + }); + return confirmAndDelete; +}; diff --git a/client/packages/requisitions/src/ResponseRequisition/api/hooks/index.ts b/client/packages/requisitions/src/ResponseRequisition/api/hooks/index.ts index 898fc5548e..831d842d0e 100644 --- a/client/packages/requisitions/src/ResponseRequisition/api/hooks/index.ts +++ b/client/packages/requisitions/src/ResponseRequisition/api/hooks/index.ts @@ -11,7 +11,8 @@ export const useResponse = { insert: Document.useInsertResponse, insertProgram: Document.useInsertProgramResponse, update: Document.useUpdateResponse, - + delete: Document.useDeleteResponses, + deleteSelected: Document.useDeleteSelectedResponseRequisitions, fields: Document.useResponseFields, }, line: { diff --git a/client/packages/requisitions/src/ResponseRequisition/api/operations.generated.ts b/client/packages/requisitions/src/ResponseRequisition/api/operations.generated.ts index d21db98a35..5b7f1e845e 100644 --- a/client/packages/requisitions/src/ResponseRequisition/api/operations.generated.ts +++ b/client/packages/requisitions/src/ResponseRequisition/api/operations.generated.ts @@ -2,9 +2,9 @@ import * as Types from '@openmsupply-client/common'; import { GraphQLClient, RequestOptions } from 'graphql-request'; import gql from 'graphql-tag'; -import { ItemRowFragmentDoc } from '../../../../system/src/Item/api/operations.generated'; -import { ReasonOptionRowFragmentDoc } from '../../../../system/src/ReasonOption/api/operations.generated'; -import { NameRowFragmentDoc } from '../../../../system/src/Name/api/operations.generated'; +import { ItemRowFragmentDoc } from 'packages/system/src/Item/api/operations.generated'; +import { NameRowFragmentDoc } from 'packages/system/src/Name/api/operations.generated'; +import { ReasonOptionRowFragmentDoc } from 'packages/system/src/ReasonOption/api/operations.generated'; type GraphQLClientRequestHeaders = RequestOptions['requestHeaders']; export type UpdateResponseMutationVariables = Types.Exact<{ storeId: Types.Scalars['String']['input']; @@ -14,6 +14,14 @@ export type UpdateResponseMutationVariables = Types.Exact<{ export type UpdateResponseMutation = { __typename: 'Mutations', updateResponseRequisition: { __typename: 'RequisitionNode', id: string } | { __typename: 'UpdateResponseRequisitionError' } }; +export type DeleteRequestMutationVariables = Types.Exact<{ + storeId: Types.Scalars['String']['input']; + input: Types.BatchResponseRequisitionInput; +}>; + + +export type DeleteRequestMutation = { __typename: 'Mutations', batchResponseRequisition: { __typename: 'BatchResponseRequisitionResponse', deleteResponseRequisitions?: Array<{ __typename: 'DeleteResponseRequisitionResponseWithId', id: string, response: { __typename: 'DeleteResponse', id: string } | { __typename: 'DeleteResponseRequisitionError', error: { __typename: 'FinalisedRequisition', description: string } | { __typename: 'LineDeleteError', description: string } | { __typename: 'RecordNotFound', description: string } | { __typename: 'RequisitionWithShipment', description: string } | { __typename: 'TransferredRequisition', description: string } } }> | null } }; + export type ResponseLineFragment = { __typename: 'RequisitionLineNode', id: string, itemId: string, requestedQuantity: number, supplyQuantity: number, remainingQuantityToSupply: number, alreadyIssued: number, comment?: string | null, averageMonthlyConsumption: number, availableStockOnHand: number, initialStockOnHandUnits: number, incomingUnits: number, outgoingUnits: number, lossInUnits: number, additionInUnits: number, expiringUnits: number, daysOutOfStock: number, optionId?: string | null, suggestedQuantity: number, requisitionNumber: number, approvedQuantity: number, approvalComment?: string | null, itemStats: { __typename: 'ItemStatsNode', availableStockOnHand: number, availableMonthsOfStockOnHand?: number | null, averageMonthlyConsumption: number }, item: { __typename: 'ItemNode', id: string, code: string, name: string, unitName?: string | null }, linkedRequisitionLine?: { __typename: 'RequisitionLineNode', itemStats: { __typename: 'ItemStatsNode', availableStockOnHand: number, averageMonthlyConsumption: number, availableMonthsOfStockOnHand?: number | null } } | null, reason?: { __typename: 'ReasonOptionNode', id: string, type: Types.ReasonOptionNodeType, reason: string, isActive: boolean } | null }; export type ResponseFragment = { __typename: 'RequisitionNode', id: string, type: Types.RequisitionNodeType, status: Types.RequisitionNodeStatus, createdDatetime: string, sentDatetime?: string | null, finalisedDatetime?: string | null, requisitionNumber: number, colour?: string | null, theirReference?: string | null, comment?: string | null, otherPartyName: string, otherPartyId: string, maxMonthsOfStock: number, minMonthsOfStock: number, approvalStatus: Types.RequisitionNodeApprovalStatus, programName?: string | null, orderType?: string | null, user?: { __typename: 'UserNode', username: string, email?: string | null } | null, shipments: { __typename: 'InvoiceConnector', totalCount: number, nodes: Array<{ __typename: 'InvoiceNode', id: string, invoiceNumber: number, createdDatetime: string, user?: { __typename: 'UserNode', username: string } | null }> }, linesRemainingToSupply: { __typename: 'RequisitionLineConnector', totalCount: number }, lines: { __typename: 'RequisitionLineConnector', totalCount: number, nodes: Array<{ __typename: 'RequisitionLineNode', id: string, itemId: string, requestedQuantity: number, supplyQuantity: number, remainingQuantityToSupply: number, alreadyIssued: number, comment?: string | null, averageMonthlyConsumption: number, availableStockOnHand: number, initialStockOnHandUnits: number, incomingUnits: number, outgoingUnits: number, lossInUnits: number, additionInUnits: number, expiringUnits: number, daysOutOfStock: number, optionId?: string | null, suggestedQuantity: number, requisitionNumber: number, approvedQuantity: number, approvalComment?: string | null, itemStats: { __typename: 'ItemStatsNode', availableStockOnHand: number, availableMonthsOfStockOnHand?: number | null, averageMonthlyConsumption: number }, item: { __typename: 'ItemNode', id: string, code: string, name: string, unitName?: string | null }, linkedRequisitionLine?: { __typename: 'RequisitionLineNode', itemStats: { __typename: 'ItemStatsNode', availableStockOnHand: number, averageMonthlyConsumption: number, availableMonthsOfStockOnHand?: number | null } } | null, reason?: { __typename: 'ReasonOptionNode', id: string, type: Types.ReasonOptionNodeType, reason: string, isActive: boolean } | null }> }, otherParty: { __typename: 'NameNode', id: string, code: string, isCustomer: boolean, isSupplier: boolean, isOnHold: boolean, name: string, store?: { __typename: 'StoreNode', id: string, code: string } | null }, period?: { __typename: 'PeriodNode', name: string, startDate: string, endDate: string } | null, linkedRequisition?: { __typename: 'RequisitionNode', id: string } | null }; @@ -289,6 +297,42 @@ export const UpdateResponseDocument = gql` } } `; +export const DeleteRequestDocument = gql` + mutation deleteRequest($storeId: String!, $input: BatchResponseRequisitionInput!) { + batchResponseRequisition(storeId: $storeId, input: $input) { + deleteResponseRequisitions { + id + response { + ... on DeleteResponseRequisitionError { + __typename + error { + description + ... on RecordNotFound { + __typename + description + } + ... on FinalisedRequisition { + __typename + description + } + ... on TransferredRequisition { + __typename + description + } + ... on RequisitionWithShipment { + __typename + description + } + } + } + ... on DeleteResponse { + id + } + } + } + } +} + `; export const ResponseByNumberDocument = gql` query responseByNumber($storeId: String!, $requisitionNumber: Int!) { requisitionByNumber( @@ -554,6 +598,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = updateResponse(variables: UpdateResponseMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(UpdateResponseDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'updateResponse', 'mutation', variables); }, + deleteRequest(variables: DeleteRequestMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(DeleteRequestDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteRequest', 'mutation', variables); + }, responseByNumber(variables: ResponseByNumberQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise { return withWrapper((wrappedRequestHeaders) => client.request(ResponseByNumberDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'responseByNumber', 'query', variables); }, diff --git a/client/packages/requisitions/src/ResponseRequisition/api/operations.graphql b/client/packages/requisitions/src/ResponseRequisition/api/operations.graphql index fbf0fdc05c..856d2ec59f 100644 --- a/client/packages/requisitions/src/ResponseRequisition/api/operations.graphql +++ b/client/packages/requisitions/src/ResponseRequisition/api/operations.graphql @@ -10,6 +10,44 @@ mutation updateResponse( } } +mutation deleteRequest( + $storeId: String! + $input: BatchResponseRequisitionInput! +) { + batchResponseRequisition(storeId: $storeId, input: $input) { + deleteResponseRequisitions { + id + response { + ... on DeleteResponseRequisitionError { + __typename + error { + description + ... on RecordNotFound { + __typename + description + } + ... on FinalisedRequisition { + __typename + description + } + ... on TransferredRequisition { + __typename + description + } + ... on RequisitionWithShipment { + __typename + description + } + } + } + ... on DeleteResponse { + id + } + } + } + } +} + fragment ResponseLine on RequisitionLineNode { id itemId diff --git a/server/graphql/batch_mutations/src/batch_response_requisition.rs b/server/graphql/batch_mutations/src/batch_response_requisition.rs index fb87881a23..c4baf65c0d 100644 --- a/server/graphql/batch_mutations/src/batch_response_requisition.rs +++ b/server/graphql/batch_mutations/src/batch_response_requisition.rs @@ -1,5 +1,6 @@ use async_graphql::*; use graphql_core::{standard_graphql_error::validate_auth, ContextExt}; +use graphql_requisition::mutations::response_requisition; use graphql_requisition_line::mutations::response_requisition_line; use service::{ auth::{Resource, ResourceAccessRequest}, @@ -16,19 +17,26 @@ type ServiceInput = BatchResponseRequisition; name = "DeleteResponseRequisitionLineResponseWithId", params(response_requisition_line::delete::DeleteResponse) ))] +#[graphql(concrete( + name = "DeleteResponseRequisitionResponseWithId", + params(response_requisition::delete::DeleteResponse) +))] pub struct MutationWithId { pub id: String, pub response: T, } -type DeleteRequisitionLinesResponse = +pub type DeleteRequisitionLinesResponse = Option>>; +pub type DeleteRequisitionsResponse = + Option>>; #[derive(SimpleObject)] #[graphql(name = "BatchResponseRequisitionResponse")] pub struct BatchResponse { delete_response_requisition_lines: DeleteRequisitionLinesResponse, + delete_response_requisitions: DeleteRequisitionsResponse, } #[derive(InputObject)] @@ -36,6 +44,7 @@ pub struct BatchResponse { pub struct BatchInput { pub delete_response_requisition_lines: Option>, + pub delete_response_requisitions: Option>, pub continue_on_error: Option, } @@ -62,21 +71,30 @@ impl BatchInput { fn to_domain(self) -> ServiceInput { let BatchInput { delete_response_requisition_lines, + delete_response_requisitions, continue_on_error, } = self; ServiceInput { delete_line: delete_response_requisition_lines .map(|inputs| inputs.into_iter().map(|input| input.to_domain()).collect()), + delete_requisition: delete_response_requisitions + .map(|inputs| inputs.into_iter().map(|input| input.to_domain()).collect()), continue_on_error, } } } impl BatchResponse { - fn from_domain(ServiceResult { delete_line }: ServiceResult) -> Result { + fn from_domain( + ServiceResult { + delete_line, + delete_requisition, + }: ServiceResult, + ) -> Result { let result = BatchResponse { delete_response_requisition_lines: map_delete_lines(delete_line)?, + delete_response_requisitions: map_delete_requisitions(delete_requisition)?, }; Ok(result) @@ -103,6 +121,25 @@ fn map_delete_lines( Ok(result.vec_or_none()) } +fn map_delete_requisitions( + responses: DeleteRequisitionsResult, +) -> Result { + let mut result = Vec::new(); + for response in responses { + let mapped_response = match response_requisition::delete::map_response(response.result) { + Ok(response) => response, + Err(standard_error) => return Err(to_standard_error(response.input, standard_error)), + }; + + result.push(MutationWithId { + id: response.input.id.clone(), + response: mapped_response, + }); + } + + Ok(result.vec_or_none()) +} + #[cfg(test)] mod test { use async_graphql::EmptyMutation; @@ -111,7 +148,10 @@ mod test { use serde_json::json; use service::{ requisition::{ - response_requisition::{BatchResponseRequisition, BatchResponseRequisitionResult}, + response_requisition::{ + BatchResponseRequisition, BatchResponseRequisitionResult, + DeleteResponseRequisition, DeleteResponseRequisitionError, + }, RequisitionServiceTrait, }, requisition_line::response_requisition_line::{ @@ -163,6 +203,16 @@ mod test { let mutation = r#" mutation mut($input: BatchResponseRequisitionInput!, $storeId: String!) { batchResponseRequisition(input: $input, storeId: $storeId) { + deleteResponseRequisitions { + id + response { + ... on DeleteResponseRequisitionError { + error { + __typename + } + } + } + } deleteResponseRequisitionLines { response { ... on DeleteResponseRequisitionLineError { @@ -180,6 +230,16 @@ mod test { let expected = json!({ "batchResponseRequisition": { + "deleteResponseRequisitions": [ + { + "id": "id7", + "response": { + "error": { + "__typename": "RecordNotFound" + } + } + } + ], "deleteResponseRequisitionLines": [ { "id": "id4", @@ -208,6 +268,12 @@ mod test { }), result: Err(DeleteResponseRequisitionLineError::RequisitionLineDoesNotExist {}), }], + delete_requisition: vec![InputWithResult { + input: inline_init(|input: &mut DeleteResponseRequisition| { + input.id = "id7".to_string() + }), + result: Err(DeleteResponseRequisitionError::RequisitionDoesNotExist {}), + }], }) })); @@ -241,6 +307,7 @@ mod test { }), result: Ok("id3".to_string()), }], + delete_requisition: Vec::new(), }) })); diff --git a/server/graphql/requisition/src/lib.rs b/server/graphql/requisition/src/lib.rs index fffd69f4b8..8ca90d43c7 100644 --- a/server/graphql/requisition/src/lib.rs +++ b/server/graphql/requisition/src/lib.rs @@ -150,6 +150,16 @@ impl RequisitionMutations { ) -> Result { response_requisition::update::update(ctx, &store_id, input) } + + async fn delete_response_requisition( + &self, + ctx: &Context<'_>, + store_id: String, + input: response_requisition::delete::DeleteInput, + ) -> Result { + response_requisition::delete::delete(ctx, &store_id, input) + } + /// Set supply quantity to requested quantity async fn supply_requested_quantity( &self, diff --git a/server/graphql/requisition/src/mutations/errors.rs b/server/graphql/requisition/src/mutations/errors.rs index 6125a625ec..b8762df2c8 100644 --- a/server/graphql/requisition/src/mutations/errors.rs +++ b/server/graphql/requisition/src/mutations/errors.rs @@ -16,3 +16,37 @@ impl MaxOrdersReachedForPeriod { "Maximum orders reached for program, order type and period" } } + +// response requisition errors + +pub struct FinalisedRequisition; +#[Object] +impl FinalisedRequisition { + pub async fn description(&self) -> &str { + "Response requisition has already been finalised" + } +} + +pub struct TransferredRequisition; +#[Object] +impl TransferredRequisition { + pub async fn description(&self) -> &str { + "Cannot delete a response requisition transferred from a request requisition" + } +} + +pub struct RequisitionWithShipment; +#[Object] +impl RequisitionWithShipment { + pub async fn description(&self) -> &str { + "Cannot delete a response requisition once a shipment has been generated" + } +} + +pub struct LineDeleteError; +#[Object] +impl LineDeleteError { + pub async fn description(&self) -> &str { + "Failed to delete lines of requisition" + } +} diff --git a/server/graphql/requisition/src/mutations/response_requisition/delete.rs b/server/graphql/requisition/src/mutations/response_requisition/delete.rs new file mode 100644 index 0000000000..5a7933f30f --- /dev/null +++ b/server/graphql/requisition/src/mutations/response_requisition/delete.rs @@ -0,0 +1,346 @@ +use async_graphql::*; +use graphql_core::{ + simple_generic_errors::RecordNotFound, standard_graphql_error::validate_auth, + standard_graphql_error::StandardGraphqlError, ContextExt, +}; +use graphql_types::types::DeleteResponse as GenericDeleteResponse; +use service::{ + auth::{Resource, ResourceAccessRequest}, + requisition::response_requisition::{ + DeleteResponseRequisition as ServiceInput, DeleteResponseRequisitionError as ServiceError, + }, +}; + +use crate::mutations::errors::{ + FinalisedRequisition, LineDeleteError, RequisitionWithShipment, TransferredRequisition, +}; + +#[derive(InputObject)] +#[graphql(name = "DeleteResponseRequisitionInput")] +pub struct DeleteInput { + pub id: String, +} + +#[derive(Interface)] +#[graphql(name = "DeleteResponseRequisitionErrorInterface")] +#[graphql(field(name = "description", ty = "String"))] +pub enum DeleteErrorInterface { + RecordNotFound(RecordNotFound), + FinalisedRequisition(FinalisedRequisition), + TransferredRequisition(TransferredRequisition), + RequisitionWithShipment(RequisitionWithShipment), + LineDeleteError(LineDeleteError), +} + +#[derive(SimpleObject)] +#[graphql(name = "DeleteResponseRequisitionError")] +pub struct DeleteError { + pub error: DeleteErrorInterface, +} + +#[derive(Union)] +#[graphql(name = "DeleteResponseRequisitionResponse")] +pub enum DeleteResponse { + Error(DeleteError), + Response(GenericDeleteResponse), +} + +pub fn delete(ctx: &Context<'_>, store_id: &str, input: DeleteInput) -> Result { + let user = validate_auth( + ctx, + &ResourceAccessRequest { + resource: Resource::MutateRequisition, + store_id: Some(store_id.to_string()), + }, + )?; + + let service_provider = ctx.service_provider(); + let service_context = service_provider.context(store_id.to_string(), user.user_id)?; + + map_response( + service_provider + .requisition_service + .delete_response_requisition(&service_context, input.to_domain()), + ) +} + +pub fn map_response(from: Result) -> Result { + let result = match from { + Ok(id) => DeleteResponse::Response(GenericDeleteResponse(id)), + Err(error) => DeleteResponse::Error(DeleteError { + error: map_error(error)?, + }), + }; + + Ok(result) +} + +impl DeleteInput { + pub fn to_domain(self) -> ServiceInput { + let DeleteInput { id } = self; + ServiceInput { id } + } +} + +fn map_error(error: ServiceError) -> Result { + use StandardGraphqlError::*; + let formatted_error = format!("{:#?}", error); + + let graphql_error = match error { + // Structured Errors + ServiceError::RequisitionDoesNotExist => { + return Ok(DeleteErrorInterface::RecordNotFound(RecordNotFound {})) + } + ServiceError::FinalisedRequisition => { + return Ok(DeleteErrorInterface::FinalisedRequisition( + FinalisedRequisition {}, + )) + } + ServiceError::TransferredRequisition => { + return Ok(DeleteErrorInterface::TransferredRequisition( + TransferredRequisition {}, + )) + } + ServiceError::RequisitionWithShipment => { + return Ok(DeleteErrorInterface::RequisitionWithShipment( + RequisitionWithShipment {}, + )) + } + ServiceError::LineDeleteError { .. } => { + return Ok(DeleteErrorInterface::LineDeleteError(LineDeleteError {})) + } + + // Standard Graphql Errors + ServiceError::NotThisStoreRequisition => BadUserInput(formatted_error), + ServiceError::NotAResponseRequisition => BadUserInput(formatted_error), + ServiceError::DatabaseError(_) => InternalError(formatted_error), + }; + + Err(graphql_error.extend()) +} + +#[cfg(test)] +mod test { + use async_graphql::EmptyMutation; + use graphql_core::{ + assert_graphql_query, assert_standard_graphql_error, test_helpers::setup_graphql_test, + }; + use repository::{mock::MockDataInserts, StorageConnectionManager}; + use serde_json::json; + use service::{ + requisition::{ + response_requisition::{ + DeleteResponseRequisition as ServiceInput, + DeleteResponseRequisitionError as ServiceError, + }, + RequisitionServiceTrait, + }, + service_provider::{ServiceContext, ServiceProvider}, + }; + + use crate::RequisitionMutations; + + type DeleteLineMethod = dyn Fn(ServiceInput) -> Result + Sync + Send; + + pub struct TestService(pub Box); + + impl RequisitionServiceTrait for TestService { + fn delete_response_requisition( + &self, + _: &ServiceContext, + input: ServiceInput, + ) -> Result { + self.0(input) + } + } + + fn service_provider( + test_service: TestService, + connection_manager: &StorageConnectionManager, + ) -> ServiceProvider { + let mut service_provider = ServiceProvider::new(connection_manager.clone(), "app_data"); + service_provider.requisition_service = Box::new(test_service); + service_provider + } + + fn empty_variables() -> serde_json::Value { + json!({ + "input": { + "id": "n/a", + }, + "storeId": "n/a" + }) + } + + #[actix_rt::test] + async fn test_graphql_delete_response_requisition_errors() { + let (_, _, connection_manager, settings) = setup_graphql_test( + EmptyMutation, + RequisitionMutations, + "test_graphql_delete_response_requisition_structured_errors", + MockDataInserts::all(), + ) + .await; + + let mutation = r#" + mutation ($input: DeleteResponseRequisitionInput!, $storeId: String) { + deleteResponseRequisition(storeId: $storeId, input: $input) { + ... on DeleteResponseRequisitionError { + error { + __typename + } + } + } + } + "#; + + // RequisitionDoesNotExist + let test_service = TestService(Box::new(|_| Err(ServiceError::RequisitionDoesNotExist))); + + let expected = json!({ + "deleteResponseRequisition": { + "error": { + "__typename": "RecordNotFound" + } + } + } + ); + + assert_graphql_query!( + &settings, + mutation, + &Some(empty_variables()), + &expected, + Some(service_provider(test_service, &connection_manager)) + ); + + // FinalisedRequisition + let test_service = TestService(Box::new(|_| Err(ServiceError::FinalisedRequisition))); + + let expected = json!({ + "deleteResponseRequisition": { + "error": { + "__typename": "FinalisedRequisition" + } + } + } + ); + + assert_graphql_query!( + &settings, + mutation, + &Some(empty_variables()), + &expected, + Some(service_provider(test_service, &connection_manager)) + ); + // NotAResponseRequisition + let test_service = TestService(Box::new(|_| Err(ServiceError::NotAResponseRequisition))); + let expected_message = "Bad user input"; + assert_standard_graphql_error!( + &settings, + &mutation, + &Some(empty_variables()), + &expected_message, + None, + Some(service_provider(test_service, &connection_manager)) + ); + + // TransferredRequisition + let test_service = TestService(Box::new(|_| Err(ServiceError::TransferredRequisition))); + + let expected = json!({ + "deleteResponseRequisition": { + "error": { + "__typename": "TransferredRequisition" + } + } + } + ); + + assert_graphql_query!( + &settings, + mutation, + &Some(empty_variables()), + &expected, + Some(service_provider(test_service, &connection_manager)) + ); + + // NotThisStoreRequisition + let test_service = TestService(Box::new(|_| Err(ServiceError::NotThisStoreRequisition))); + let expected_message = "Bad user input"; + assert_standard_graphql_error!( + &settings, + &mutation, + &Some(empty_variables()), + &expected_message, + None, + Some(service_provider(test_service, &connection_manager)) + ); + + // NotAResponseRequisition + let test_service = TestService(Box::new(|_| Err(ServiceError::NotAResponseRequisition))); + let expected_message = "Bad user input"; + assert_standard_graphql_error!( + &settings, + &mutation, + &Some(empty_variables()), + &expected_message, + None, + Some(service_provider(test_service, &connection_manager)) + ); + } + + #[actix_rt::test] + async fn test_graphql_delete_response_requisition_success() { + let (_, _, connection_manager, settings) = setup_graphql_test( + EmptyMutation, + RequisitionMutations, + "test_graphql_delete_response_requisition_success", + MockDataInserts::all(), + ) + .await; + + let mutation = r#" + mutation ($storeId: String, $input: DeleteResponseRequisitionInput!) { + deleteResponseRequisition(storeId: $storeId, input: $input) { + ... on DeleteResponse { + id + } + } + } + "#; + + // Success + let test_service = TestService(Box::new(|input| { + assert_eq!( + input, + ServiceInput { + id: "id input".to_string(), + } + ); + Ok("deleted id".to_owned()) + })); + + let variables = json!({ + "input": { + "id": "id input", + }, + "storeId": "store_a" + }); + + let expected = json!({ + "deleteResponseRequisition": { + "id": "deleted id" + } + } + ); + + assert_graphql_query!( + &settings, + mutation, + &Some(variables), + &expected, + Some(service_provider(test_service, &connection_manager)) + ); + } +} diff --git a/server/graphql/requisition/src/mutations/response_requisition/mod.rs b/server/graphql/requisition/src/mutations/response_requisition/mod.rs index 7d3d3bcd37..3dcacea8af 100644 --- a/server/graphql/requisition/src/mutations/response_requisition/mod.rs +++ b/server/graphql/requisition/src/mutations/response_requisition/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod create_requisition_shipment; +pub mod delete; pub(crate) mod insert; pub(crate) mod insert_program; pub(crate) mod supply_requested_quantity; diff --git a/server/service/src/requisition/mod.rs b/server/service/src/requisition/mod.rs index 084e7d2084..15273fb2d3 100644 --- a/server/service/src/requisition/mod.rs +++ b/server/service/src/requisition/mod.rs @@ -31,7 +31,8 @@ use repository::{ RequisitionFilter, RequisitionLine, RequisitionSort, }; use response_requisition::{ - batch_response_requisition, BatchResponseRequisition, BatchResponseRequisitionResult, + batch_response_requisition, delete_response_requisition, BatchResponseRequisition, + BatchResponseRequisitionResult, DeleteResponseRequisition, DeleteResponseRequisitionError, }; pub mod common; @@ -152,6 +153,14 @@ pub trait RequisitionServiceTrait: Sync + Send { update_response_requisition(ctx, input) } + fn delete_response_requisition( + &self, + ctx: &ServiceContext, + input: DeleteResponseRequisition, + ) -> Result { + delete_response_requisition(ctx, input) + } + fn supply_requested_quantity( &self, ctx: &ServiceContext, diff --git a/server/service/src/requisition/response_requisition/batch.rs b/server/service/src/requisition/response_requisition/batch.rs index d3a144f3df..f3a0fd01c2 100644 --- a/server/service/src/requisition/response_requisition/batch.rs +++ b/server/service/src/requisition/response_requisition/batch.rs @@ -5,9 +5,14 @@ use crate::{ BatchMutationsProcessor, InputWithResult, WithDBError, }; +use super::{ + delete_response_requisition, DeleteResponseRequisition, DeleteResponseRequisitionError, +}; + #[derive(Clone)] pub struct BatchResponseRequisition { pub delete_line: Option>, + pub delete_requisition: Option>, pub continue_on_error: Option, } @@ -18,9 +23,13 @@ pub type DeleteRequisitionLinesResult = Vec< >, >; +pub type DeleteRequisitionsResult = + Vec>>; + #[derive(Debug, Default)] pub struct BatchResponseRequisitionResult { pub delete_line: DeleteRequisitionLinesResult, + pub delete_requisition: DeleteRequisitionsResult, } pub fn batch_response_requisition( @@ -42,6 +51,13 @@ pub fn batch_response_requisition( return Err(WithDBError::err(results)); } + let (has_errors, result) = mutations_processor + .do_mutations(input.delete_requisition, delete_response_requisition); + results.delete_requisition = result; + if has_errors && !continue_on_error { + return Err(WithDBError::err(results)); + } + Ok(results) as Result< BatchResponseRequisitionResult, @@ -92,6 +108,7 @@ mod test { |input: &mut DeleteResponseRequisitionLine| input.id = line_id.clone(), )]), continue_on_error: None, + delete_requisition: None, }; assert_eq!( diff --git a/server/service/src/requisition/response_requisition/delete.rs b/server/service/src/requisition/response_requisition/delete.rs new file mode 100644 index 0000000000..e0e26afe81 --- /dev/null +++ b/server/service/src/requisition/response_requisition/delete.rs @@ -0,0 +1,278 @@ +use repository::{ + requisition_row::{RequisitionStatus, RequisitionType}, + EqualFilter, InvoiceFilter, InvoiceRepository, RepositoryError, RequisitionLineFilter, + RequisitionLineRepository, RequisitionRowRepository, StorageConnection, +}; + +use crate::{ + requisition::common::check_requisition_row_exists, + requisition_line::response_requisition_line::{ + delete_response_requisition_line, DeleteResponseRequisitionLine, + DeleteResponseRequisitionLineError, + }, + service_provider::ServiceContext, +}; + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct DeleteResponseRequisition { + pub id: String, +} + +#[derive(Debug, PartialEq)] + +pub enum DeleteResponseRequisitionError { + RequisitionDoesNotExist, + NotThisStoreRequisition, + FinalisedRequisition, + NotAResponseRequisition, + TransferredRequisition, + RequisitionWithShipment, + LineDeleteError { + line_id: String, + error: DeleteResponseRequisitionLineError, + }, + DatabaseError(RepositoryError), +} + +type OutError = DeleteResponseRequisitionError; + +pub fn delete_response_requisition( + ctx: &ServiceContext, + input: DeleteResponseRequisition, +) -> Result { + let requisition_id = ctx + .connection + .transaction_sync(|connection| { + validate(connection, &ctx.store_id, &input)?; + + let lines = RequisitionLineRepository::new(connection).query_by_filter( + RequisitionLineFilter::new().requisition_id(EqualFilter::equal_to(&input.id)), + )?; + for line in lines { + delete_response_requisition_line( + ctx, + DeleteResponseRequisitionLine { + id: line.requisition_line_row.id.clone(), + }, + ) + .map_err(|error| { + DeleteResponseRequisitionError::LineDeleteError { + line_id: line.requisition_line_row.id, + error, + } + })?; + } + + match RequisitionRowRepository::new(connection).delete(&input.id) { + Ok(_) => Ok(input.id.clone()), + Err(error) => Err(OutError::DatabaseError(error)), + } + }) + .map_err(|error| error.to_inner_error())?; + Ok(requisition_id) +} + +fn validate( + connection: &StorageConnection, + store_id: &str, + input: &DeleteResponseRequisition, +) -> Result<(), OutError> { + let requisition_row = check_requisition_row_exists(connection, &input.id)? + .ok_or(OutError::RequisitionDoesNotExist)?; + + if requisition_row.store_id != store_id { + return Err(OutError::NotThisStoreRequisition); + } + + if requisition_row.r#type != RequisitionType::Response { + return Err(OutError::NotAResponseRequisition); + } + + if requisition_row.status == RequisitionStatus::Finalised { + return Err(OutError::FinalisedRequisition); + } + + if requisition_row.linked_requisition_id.is_some() { + return Err(OutError::TransferredRequisition); + } + + let filter = InvoiceFilter { + requisition_id: Some(EqualFilter::equal_to(&requisition_row.id)), + ..Default::default() + }; + if InvoiceRepository::new(connection) + .query_one(filter)? + .is_some() + { + return Err(OutError::RequisitionWithShipment); + } + + Ok(()) +} + +impl From for DeleteResponseRequisitionError { + fn from(error: RepositoryError) -> Self { + DeleteResponseRequisitionError::DatabaseError(error) + } +} + +#[cfg(test)] +mod test_delete { + + use chrono::NaiveDateTime; + use repository::{ + mock::{ + mock_new_response_requisition, mock_request_draft_requisition, + mock_sent_request_requisition, mock_store_a, mock_store_b, MockDataInserts, + }, + test_db::setup_all, + InvoiceRow, InvoiceRowRepository, InvoiceStatus, InvoiceType, RequisitionRow, + RequisitionRowRepository, RequisitionStatus, RequisitionType, + }; + + use crate::{ + requisition::response_requisition::{ + DeleteResponseRequisition, DeleteResponseRequisitionError as ServiceError, + }, + service_provider::ServiceProvider, + }; + + #[actix_rt::test] + async fn delete_response_requisition_errors() { + let (_, connection, connection_manager, _) = + setup_all("delete_response_requisition_errors", MockDataInserts::all()).await; + + let service_provider = ServiceProvider::new(connection_manager, "app_data"); + let mut context = service_provider + .context(mock_store_a().id, "".to_string()) + .unwrap(); + let service = service_provider.requisition_service; + let requisition_repo = RequisitionRowRepository::new(&connection); + let invoice_repo = InvoiceRowRepository::new(&connection); + + // RequisitionDoesNotExist + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: "invalid".to_owned(), + }, + ), + Err(ServiceError::RequisitionDoesNotExist) + ); + + // NotAResponseRequisition, + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: mock_sent_request_requisition().id, + }, + ), + Err(ServiceError::NotAResponseRequisition) + ); + + // FinalisedRequisition, + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: mock_request_draft_requisition().id, + }, + ), + Err(ServiceError::NotAResponseRequisition) + ); + + // TransferredRequisition, + let transfer_requisition = RequisitionRow { + id: "transfer_requisition".to_string(), + requisition_number: 3, + name_link_id: "name_a".to_string(), + store_id: mock_store_a().id, + r#type: RequisitionType::Response, + status: RequisitionStatus::New, + linked_requisition_id: Some(mock_sent_request_requisition().id), + ..Default::default() + }; + requisition_repo.upsert_one(&transfer_requisition).unwrap(); + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: "transfer_requisition".to_string(), + }, + ), + Err(ServiceError::TransferredRequisition) + ); + + // RequisitionWithShipment + let invoice = InvoiceRow { + id: "invoice_id".to_string(), + name_link_id: "name_a".to_string(), + store_id: mock_store_a().id, + invoice_number: 3, + r#type: InvoiceType::OutboundShipment, + status: InvoiceStatus::New, + on_hold: false, + created_datetime: NaiveDateTime::parse_from_str( + "2021-01-02T00:00:00", + "%Y-%m-%dT%H:%M:%S", + ) + .unwrap(), + currency_rate: 1.0, + requisition_id: Some(mock_new_response_requisition().id), + ..Default::default() + }; + invoice_repo.upsert_one(&invoice).unwrap(); + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: mock_new_response_requisition().id, + }, + ), + Err(ServiceError::RequisitionWithShipment) + ); + + // NotThisStoreRequisition + context.store_id = mock_store_b().id; + assert_eq!( + service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: mock_new_response_requisition().id, + }, + ), + Err(ServiceError::NotThisStoreRequisition) + ); + } + + #[actix_rt::test] + async fn delete_response_requisition_success() { + let (_, connection, connection_manager, _) = setup_all( + "delete_response_requisition_success", + MockDataInserts::all(), + ) + .await; + + let service_provider = ServiceProvider::new(connection_manager, "app_data"); + let context = service_provider + .context(mock_store_a().id, "".to_string()) + .unwrap(); + let service = service_provider.requisition_service; + + let result = service.delete_response_requisition( + &context, + DeleteResponseRequisition { + id: mock_new_response_requisition().id, + }, + ); + + assert_eq!( + RequisitionRowRepository::new(&connection) + .find_one_by_id(&result.unwrap()) + .unwrap(), + None + ) + } +} diff --git a/server/service/src/requisition/response_requisition/mod.rs b/server/service/src/requisition/response_requisition/mod.rs index 2e90d41a63..4760b5e5e1 100644 --- a/server/service/src/requisition/response_requisition/mod.rs +++ b/server/service/src/requisition/response_requisition/mod.rs @@ -4,6 +4,9 @@ pub use self::update::*; mod insert; pub use self::insert::*; +mod delete; +pub use self::delete::*; + mod supply_requested_quantity; pub use supply_requested_quantity::*;