Skip to content

Commit e8ea753

Browse files
authored
UIOR-1568 Handle the budgetNotFoundForFiscalYear error code based on the intended PO action (#1876)
* UIOR-1568 Handle the `budgetNotFoundForFiscalYear` error code based on the intended PO action * add tests
1 parent b66dcf0 commit e8ea753

13 files changed

Lines changed: 560 additions & 142 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Include prepayment information in order CSV export. Refs UIOR-1535.
1010
* Deprecate acquisition methods in settings, PO line and templates. Refs UIOR-1157.
1111
* Do not copy closure reason when duplicating or reopening an order. Refs UIOR-1567.
12+
* Handle the `budgetNotFoundForFiscalYear` error code based on the intended PO action. Refs UIOR-1568.
1213

1314
## [9.0.5](https://github.com/folio-org/ui-orders/tree/v9.0.5) (2026-07-20)
1415
[Full Changelog](https://github.com/folio-org/ui-orders/compare/v9.0.4...v9.0.5)

src/common/constants/po.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,13 @@ export const PO_FORM_FIELDS = {
2525
vendor: 'vendor',
2626
workflowStatus: 'workflowStatus',
2727
};
28+
29+
export const PO_UPDATE_ACTION_TYPES = {
30+
APPROVE: 'approve',
31+
CANCEL: 'cancel',
32+
CLOSE: 'close',
33+
OPEN: 'open',
34+
RE_ENCUMBER: 're-encumber',
35+
REOPEN: 'reopen',
36+
UNOPEN: 'unopen',
37+
};

src/common/hooks/useHandleOrderUpdateError/useHandleOrderUpdateError.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ const useHandleOrderUpdateError = (mutatorExpenseClass) => {
1212
const sendCallout = useShowCallout();
1313

1414
// this is required to avoid huge refactoring of processing error messages for now
15-
const context = useMemo(() => ({ sendCallout }), [sendCallout]);
15+
const callout = useMemo(() => ({ sendCallout }), [sendCallout]);
1616

17-
const handleErrorResponse = useCallback(async (response, orderErrorModalShow, defaultCode, toggleDeletePieces) => {
17+
const handleErrorResponse = useCallback(async (response, options = {}) => {
1818
try {
1919
const { errors } = await response.clone().json();
2020
const errorCode = errors?.[0]?.code;
@@ -33,13 +33,13 @@ const useHandleOrderUpdateError = (mutatorExpenseClass) => {
3333
});
3434
}
3535
} else {
36-
await showUpdateOrderError(response, context, orderErrorModalShow, defaultCode, toggleDeletePieces);
36+
await showUpdateOrderError(response, { callout, ...options });
3737
}
38-
} catch (e) {
39-
await showUpdateOrderError(response, context);
38+
} catch {
39+
await showUpdateOrderError(response, { callout, ...options });
4040
}
4141
throw new Error('Order update error');
42-
}, [context, mutator, sendCallout]);
42+
}, [callout, mutator, sendCallout]);
4343

4444
return [handleErrorResponse];
4545
};

src/common/hooks/useHandleOrderUpdateError/useHandleOrderUpdateError.test.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ const getMockResponse = (code = 'inactiveExpenseClass', key = 'expenseClassId')
2828
}),
2929
});
3030

31+
const getInvalidResponse = () => ({
32+
clone: () => ({
33+
json: () => {
34+
throw new Error('Invalid response');
35+
},
36+
}),
37+
});
38+
3139
describe('useHandleOrderUpdateError', () => {
3240
const sendCallout = jest.fn();
3341

@@ -57,13 +65,49 @@ describe('useHandleOrderUpdateError', () => {
5765

5866
it('should handle response with another error code', async () => {
5967
const { result } = renderHook(() => useHandleOrderUpdateError(mutator));
68+
const openModal = jest.fn();
69+
const onNoBudgetForFiscalYear = jest.fn();
6070

6171
try {
62-
await result.current[0](() => getMockResponse(''));
72+
await result.current[0](getMockResponse('genericError'), {
73+
openModal,
74+
genericCode: 'custom.error',
75+
actionType: 'onNoBudgetForFiscalYear',
76+
onNoBudgetForFiscalYear,
77+
});
6378

64-
expect(showUpdateOrderError).toHaveBeenCalled();
79+
expect(showUpdateOrderError).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
80+
openModal,
81+
genericCode: 'custom.error',
82+
actionType: 'onNoBudgetForFiscalYear',
83+
onNoBudgetForFiscalYear,
84+
}));
6585
} catch (e) {
6686
expect(e.message).toEqual('Order update error');
6787
}
6888
});
89+
90+
it('should forward all options when response parsing fails', async () => {
91+
const { result } = renderHook(() => useHandleOrderUpdateError(mutator));
92+
const openModal = jest.fn();
93+
const toggleDeletePieces = jest.fn();
94+
95+
try {
96+
await result.current[0](getInvalidResponse(), {
97+
openModal,
98+
genericCode: 'custom.error',
99+
toggleDeletePieces,
100+
actionType: 'approve',
101+
});
102+
} catch (e) {
103+
expect(e.message).toEqual('Order update error');
104+
}
105+
106+
expect(showUpdateOrderError).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
107+
openModal,
108+
genericCode: 'custom.error',
109+
toggleDeletePieces,
110+
actionType: 'approve',
111+
}));
112+
});
69113
});

src/components/LayerCollection/LayerPO.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function LayerPO({
135135
}
136136
})
137137
.catch(async e => {
138-
await handleErrorResponse(e, openOrderErrorModalShow);
138+
await handleErrorResponse(e, { openModal: openOrderErrorModalShow });
139139
})
140140
.finally(() => setIsLoading(false));
141141
}, [

src/components/PurchaseOrder/PO.js

Lines changed: 55 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import get from 'lodash/get';
2-
import PropTypes from 'prop-types';
32
import {
43
useCallback,
54
useEffect,
@@ -11,15 +10,13 @@ import {
1110
FormattedMessage,
1211
useIntl,
1312
} from 'react-intl';
14-
import ReactRouterPropTypes from 'react-router-prop-types';
1513

1614
import {
1715
IfPermission,
1816
TitleManager,
1917
stripesConnect,
2018
} from '@folio/stripes/core';
2119
import {
22-
baseManifest,
2320
CUSTOM_FIELDS_ORDERS_BACKEND_NAME,
2421
getErrorCodeFromResponse,
2522
handleKeyCommand,
@@ -69,16 +66,13 @@ import {
6966
INVOICES_ROUTE,
7067
ORDERS_ROUTE,
7168
PO_CONFIG_NAME_PREFIX,
69+
PO_UPDATE_ACTION_TYPES,
7270
REEXPORT_SOURCES,
7371
SCOPE_CUSTOM_FIELDS_MANAGE,
7472
WORKFLOW_STATUS,
7573
} from '../../common/constants';
7674
import { useHandleOrderUpdateError } from '../../common/hooks';
7775
import { isOngoing } from '../../common/POFields';
78-
import {
79-
reasonsForClosureResource,
80-
updateEncumbrancesResource,
81-
} from '../../common/resources';
8276
import {
8377
getCommonErrorMessage,
8478
getExportAccountNumbers,
@@ -93,15 +87,12 @@ import {
9387
reopenOrder as reopenOrderResource,
9488
updateOrderResource,
9589
} from '../Utils/orderResource';
96-
import {
97-
APPROVALS_SETTING,
98-
FUND,
99-
LINES_LIMIT,
100-
ORDER_NUMBER,
101-
ORDER,
102-
} from '../Utils/resources';
10390
import CloseOrderModal from './CloseOrder';
104-
import { LINE_LISTING_COLUMN_MAPPING } from './constants';
91+
import {
92+
LINE_LISTING_COLUMN_MAPPING,
93+
PO_MANIFEST,
94+
PO_PROP_TYPES,
95+
} from './constants';
10596
import { getPOActionMenu } from './getPOActionMenu';
10697
import {
10798
useOrderMutation,
@@ -260,7 +251,7 @@ const PO = ({
260251
refreshList();
261252
})
262253
.catch(e => {
263-
return handleErrorResponse(e, orderErrorModalShow, 'clone.error');
254+
return handleErrorResponse(e, { openModal: orderErrorModalShow, genericCode: 'clone.error' });
264255
})
265256
.finally(() => setIsLoading(false));
266257
}, [
@@ -324,6 +315,8 @@ const PO = ({
324315
},
325316
};
326317

318+
const actionType = isCancelReason ? PO_UPDATE_ACTION_TYPES.CANCEL : PO_UPDATE_ACTION_TYPES.CLOSE;
319+
327320
setIsCancelReason(false);
328321
toggleCloseOrderModal();
329322
setIsLoading(true);
@@ -336,19 +329,24 @@ const PO = ({
336329

337330
return refetch();
338331
},
339-
e => handleErrorResponse(e, orderErrorModalShow, 'closeOrder'),
332+
(e) => handleErrorResponse(e, {
333+
actionType,
334+
genericCode: 'closeOrder',
335+
openModal: orderErrorModalShow,
336+
}),
340337
)
341338
.finally(() => setIsLoading(false));
342339
}, [
343-
toggleCloseOrderModal,
344-
order,
340+
handleErrorResponse,
341+
isCancelReason,
345342
mutator.orderDetails,
346-
sendCallout,
347-
refreshList,
343+
order,
344+
orderErrorModalShow,
348345
refetch,
349346
refetchFiscalYears,
350-
handleErrorResponse,
351-
orderErrorModalShow,
347+
refreshList,
348+
sendCallout,
349+
toggleCloseOrderModal,
352350
]);
353351

354352
const cancelClosingOrder = useCallback(() => {
@@ -368,8 +366,11 @@ const PO = ({
368366

369367
return refetch();
370368
},
371-
e => {
372-
return handleErrorResponse(e, orderErrorModalShow);
369+
(e) => {
370+
return handleErrorResponse(e, {
371+
actionType: PO_UPDATE_ACTION_TYPES.APPROVE,
372+
openModal: orderErrorModalShow,
373+
});
373374
},
374375
)
375376
.finally(() => setIsLoading(false));
@@ -414,8 +415,13 @@ const PO = ({
414415

415416
return refetch();
416417
},
417-
e => {
418-
return handleErrorResponse(e, orderErrorModalShow, ERROR_CODES.orderGenericError1, toggleDeletePieces);
418+
(e) => {
419+
return handleErrorResponse(e, {
420+
actionType: PO_UPDATE_ACTION_TYPES.OPEN,
421+
genericCode: ERROR_CODES.orderGenericError1,
422+
openModal: orderErrorModalShow,
423+
toggleDeletePieces,
424+
});
419425
},
420426
)
421427
.finally(() => setIsLoading(false));
@@ -451,8 +457,11 @@ const PO = ({
451457

452458
return refetch();
453459
},
454-
e => {
455-
return handleErrorResponse(e, orderErrorModalShow);
460+
(e) => {
461+
return handleErrorResponse(e, {
462+
actionType: PO_UPDATE_ACTION_TYPES.REOPEN,
463+
openModal: orderErrorModalShow,
464+
});
456465
},
457466
)
458467
.finally(() => setIsLoading(false));
@@ -491,7 +500,10 @@ const PO = ({
491500
return refetch();
492501
},
493502
(e) => {
494-
return handleErrorResponse(e?.response, orderErrorModalShow);
503+
return handleErrorResponse(e?.response, {
504+
actionType: PO_UPDATE_ACTION_TYPES.UNOPEN,
505+
openModal: orderErrorModalShow,
506+
});
495507
},
496508
)
497509
.finally(() => setIsLoading(false));
@@ -519,8 +531,11 @@ const PO = ({
519531
search: location.search,
520532
});
521533
})
522-
.catch(e => {
523-
return handleErrorResponse(e, orderErrorModalShow, 'noCreatedOrder');
534+
.catch((e) => {
535+
return handleErrorResponse(e, {
536+
genericCode: 'noCreatedOrder',
537+
openModal: orderErrorModalShow,
538+
});
524539
})
525540
.finally(() => setIsLoading(false));
526541
}, [
@@ -623,8 +638,12 @@ const PO = ({
623638

624639
return refetch();
625640
},
626-
e => {
627-
return handleErrorResponse(e, orderErrorModalShow, 'ui-orders.order.updateEncumbrances.error');
641+
(e) => {
642+
return handleErrorResponse(e, {
643+
actionType: PO_UPDATE_ACTION_TYPES.RE_ENCUMBER,
644+
genericCode: 'ui-orders.order.updateEncumbrances.error',
645+
openModal: orderErrorModalShow,
646+
});
628647
},
629648
)
630649
.finally(() => setIsLoading(false));
@@ -973,33 +992,7 @@ const PO = ({
973992
);
974993
};
975994

976-
PO.manifest = Object.freeze({
977-
orderDetails: {
978-
...ORDER,
979-
accumulate: true,
980-
fetch: false,
981-
},
982-
linesLimit: LINES_LIMIT,
983-
closingReasons: reasonsForClosureResource,
984-
fund: FUND,
985-
approvalsSetting: APPROVALS_SETTING,
986-
expenseClass: {
987-
...baseManifest,
988-
accumulate: true,
989-
fetch: false,
990-
},
991-
generatedOrderNumber: ORDER_NUMBER,
992-
updateEncumbrances: updateEncumbrancesResource,
993-
});
994-
995-
PO.propTypes = {
996-
history: ReactRouterPropTypes.history.isRequired,
997-
location: ReactRouterPropTypes.location.isRequired,
998-
match: ReactRouterPropTypes.match.isRequired,
999-
mutator: PropTypes.object.isRequired,
1000-
resources: PropTypes.object.isRequired,
1001-
refreshList: PropTypes.func.isRequired,
1002-
stripes: PropTypes.object.isRequired,
1003-
};
995+
PO.manifest = PO_MANIFEST;
996+
PO.propTypes = PO_PROP_TYPES;
1004997

1005998
export default stripesConnect(PO);

0 commit comments

Comments
 (0)