diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e913cde2e6e8..678b7dbbcd64 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1408,6 +1408,10 @@ const CONST = { PAY: 'pay', FIX: 'fix', }, + ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE: { + EXPENSE: 'expense', + TASK: 'task', + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5484e8625d3c..da8483d5d0f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -89,7 +89,7 @@ import {getBankAccountFromID} from './actions/BankAccounts'; import {createDraftTransaction, setMoneyRequestParticipants, setMoneyRequestParticipantsFromReport, setMoneyRequestReportID, startDistanceRequest, startMoneyRequest} from './actions/IOU'; import type {IOURequestType} from './actions/IOU'; import {unholdRequest} from './actions/IOU/Hold'; -import {canApproveIOU, canIOUBePaid, canSubmitReport, getIOUReportActionWithBadge} from './actions/IOU/ReportWorkflow'; +import {canApproveIOU, canIOUBePaid, canSubmitReport, getBadgeFromIOUReport, getIOUReportActionWithBadge} from './actions/IOU/ReportWorkflow'; import {createDraftWorkspace} from './actions/Policy/Policy'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; import {openUnreportedExpense} from './actions/Report'; @@ -4196,27 +4196,30 @@ function getLastVisibleMessage( } /** - * Checks if a report is waiting for the manager to complete an action. + * Returns the action type for the assignee to complete, or undefined if there is no pending action. * Example: the assignee of an open task report or the manager of a processing expense report. * * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForAssigneeToCompleteAction(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { +function getActionTypeForAssigneeToComplete( + report: OnyxEntry, + parentReportAction: OnyxEntry, +): ValueOf | undefined { if (report?.hasOutstandingChildTask) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.TASK; } if (report?.hasParentAccess === false && isReportManager(report)) { if (isOpenTaskReport(report, parentReportAction)) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.TASK; } if (isProcessingReport(report) && isExpenseReport(report)) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; } } - return false; + return undefined; } function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): boolean { @@ -4299,7 +4302,19 @@ function getReasonAndReportActionThatRequiresAttention( }; } - if (isWaitingForAssigneeToCompleteAction(optionOrReport, parentReportAction)) { + const optionReportMetadata = allReportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${optionOrReport.reportID}`]; + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const policy = getPolicy(optionOrReport.policyID); + const invoiceReceiverPolicyID = optionOrReport?.invoiceReceiver && 'policyID' in optionOrReport.invoiceReceiver ? optionOrReport.invoiceReceiver.policyID : undefined; + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; + const actionTypeForAssigneeToComplete = getActionTypeForAssigneeToComplete(optionOrReport, parentReportAction); + + if (actionTypeForAssigneeToComplete) { + const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; + const expenseBadge = isAssigneeExpenseAction ? getBadgeFromIOUReport(optionOrReport, undefined, policy, optionReportMetadata, invoiceReceiverPolicy) : undefined; return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, reportAction: Object.values(reportActions) @@ -4307,17 +4322,10 @@ function getReasonAndReportActionThatRequiresAttention( // eslint-disable-next-line rulesdir/prefer-locale-compare-from-context .sort((a, b) => (!a.created || !b.created ? 0 : a.created.localeCompare(b.created))) .at(0), + ...(expenseBadge ? {actionBadge: expenseBadge} : {}), }; } - const optionReportMetadata = allReportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${optionOrReport.reportID}`]; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(optionOrReport.policyID); - const invoiceReceiverPolicyID = optionOrReport?.invoiceReceiver && 'policyID' in optionOrReport.invoiceReceiver ? optionOrReport.invoiceReceiver.policyID : undefined; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); @@ -13754,7 +13762,7 @@ export { isUserCreatedPolicyRoom, isValidReport, isValidReportIDFromPath, - isWaitingForAssigneeToCompleteAction, + getActionTypeForAssigneeToComplete, isWaitingForSubmissionFromCurrentUser, isWorkspaceMemberLeavingWorkspaceRoom, isInvoiceRoom, diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 1f433f296c54..1eff42a99853 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -262,6 +262,39 @@ function canSubmitReport( ); } +function getBadgeFromIOUReport( + iouReport: OnyxEntry, + chatReport: OnyxEntry, + policy: OnyxEntry, + reportMetadata: OnyxEntry, + invoiceReceiverPolicy: OnyxEntry, +): ValueOf | undefined { + // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses + if ( + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) + ) { + return CONST.REPORT.ACTION_BADGE.PAY; + } + if (canApproveIOU(iouReport, policy, reportMetadata)) { + return CONST.REPORT.ACTION_BADGE.APPROVE; + } + const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( + iouReport, + chatReport, + policy, + getReportTransactions(iouReport?.reportID), + getAllTransactionViolations(), + getCurrentUserEmail(), + getUserAccountID(), + getAllReportActions(iouReport?.reportID), + ); + if (isWaitingSubmitFromCurrentUser) { + return CONST.REPORT.ACTION_BADGE.SUBMIT; + } + return undefined; +} + function getIOUReportActionWithBadge( chatReport: OnyxEntry, policy: OnyxEntry, @@ -276,30 +309,9 @@ function getIOUReportActionWithBadge( return false; } const iouReport = getReportOrDraftReport(action.childReportID); - // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses - if ( - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) - ) { - actionBadge = CONST.REPORT.ACTION_BADGE.PAY; - return true; - } - if (canApproveIOU(iouReport, policy, reportMetadata)) { - actionBadge = CONST.REPORT.ACTION_BADGE.APPROVE; - return true; - } - const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( - iouReport, - chatReport, - policy, - getReportTransactions(iouReport?.reportID), - getAllTransactionViolations(), - getCurrentUserEmail(), - getUserAccountID(), - getAllReportActions(iouReport?.reportID), - ); - if (isWaitingSubmitFromCurrentUser) { - actionBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; + const badge = getBadgeFromIOUReport(iouReport, chatReport, policy, reportMetadata, invoiceReceiverPolicy); + if (badge) { + actionBadge = badge; return true; } return false; @@ -1740,6 +1752,7 @@ export { canSubmitReport, canUnapproveIOU, determineIouReportID, + getBadgeFromIOUReport, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, reopenReport, diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index d18f72c54b2d..7f46f10a145f 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -11,6 +11,7 @@ import { canCancelPayment, canIOUBePaid, canUnapproveIOU, + getBadgeFromIOUReport, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, retractReport, @@ -2488,80 +2489,75 @@ describe('actions/IOU/ReportWorkflow', () => { describe('getIOUReportActionWithBadge', () => { it('should exclude deleted actions', async () => { - const reportID = '1'; - const policyID = '2'; + const chatReportID = '1'; + const iouReportID = '2'; + const policyID = '3'; const fakePolicy: Policy = { ...createRandomPolicy(Number(policyID)), + id: policyID, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, type: CONST.POLICY.TYPE.TEAM, }; - const fakeReport: Report = { - ...createRandomReport(Number(reportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, type: CONST.REPORT.TYPE.EXPENSE, policyID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: RORY_ACCOUNT_ID, }; - const fakeTransaction1: Transaction = { + + const fakeTransaction: Transaction = { ...createRandomTransaction(0), - reportID, - bank: CONST.EXPENSIFY_CARD.BANK, - status: CONST.TRANSACTION.STATUS.PENDING, - }; - const fakeTransaction2: Transaction = { - ...createRandomTransaction(1), - reportID, - amount: 27, - receipt: { - source: 'test', - state: CONST.IOU.RECEIPT_STATE.SCANNING, - }, - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - modifiedMerchant: undefined, - }; - const fakeTransaction3: Transaction = { - ...createRandomTransaction(2), - reportID, + reportID: iouReportID, amount: 100, status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction3.transactionID}`, fakeTransaction3); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); - await waitForBatchedUpdates(); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); const deletedReportAction = { reportActionID: '0', actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, - created: '2024-08-08 18:70:44.171', - childReportID: reportID, + created: '2024-08-08 18:00:00.000', + childReportID: iouReportID, + }; + + const validReportAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [ + { + type: 'TEXT', + text: 'Hello world!', + }, + ], }; const MOCK_REPORT_ACTIONS: ReportActions = { [deletedReportAction.reportActionID]: deletedReportAction, - [reportID]: { - reportActionID: reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, - created: '2024-08-08 19:70:44.171', - childReportID: reportID, - message: [ - { - type: 'TEXT', - text: 'Hello world!', - }, - ], - }, + [validReportAction.reportActionID]: validReportAction, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, MOCK_REPORT_ACTIONS); + await waitForBatchedUpdates(); - const result = getIOUReportActionWithBadge(fakeReport, fakePolicy, {}, undefined); - expect(result.reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + const result = getIOUReportActionWithBadge(fakeChatReport, fakePolicy, {}, undefined); + expect(result.reportAction).toMatchObject(validReportAction); expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); }); @@ -2943,4 +2939,139 @@ describe('actions/IOU/ReportWorkflow', () => { expect(result.actionBadge).toBeUndefined(); }); }); + + describe('getBadgeFromIOUReport', () => { + it('should return APPROVE badge for submitted expense report when user is manager', async () => { + const iouReportID = '1100'; + const chatReportID = '1101'; + const policyID = '1102'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return PAY badge for approved expense report when user is payer', async () => { + const iouReportID = '1200'; + const chatReportID = '1201'; + const policyID = '1202'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: -10000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should return undefined badge for settled report', async () => { + const iouReportID = '1300'; + const chatReportID = '1301'; + const policyID = '1302'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + managerID: RORY_ACCOUNT_ID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBeUndefined(); + }); + }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index ec1fd060ec05..6cf255e4bfee 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8892,6 +8892,50 @@ describe('ReportUtils', () => { // Then the result is null expect(result).toBe(null); }); + it('should return IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION with actionBadge for expense report with hasParentAccess=false', async () => { + const policyID = 'testPolicy123'; + const fakePolicy: Policy = { + ...createRandomPolicy(1), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + type: CONST.POLICY.TYPE.TEAM, + }; + + // An expense report with hasParentAccess=false, in processing state, where current user is the manager + const expenseReport: Report = { + ...createExpenseReport(60000), + policyID, + hasParentAccess: false, + managerID: currentUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + type: CONST.REPORT.TYPE.EXPENSE, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: expenseReport.reportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + // Ensure session is set (may have been cleared by a previous test) + await Onyx.merge(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const {result: isReportArchived} = renderHook(() => useReportIsArchived(expenseReport?.reportID)); + const result = getReasonAndReportActionThatRequiresAttention(expenseReport, undefined, isReportArchived.current); + + // Should return IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION (from getActionTypeForAssigneeToComplete) + // AND include actionBadge (APPROVE) since it's a processing expense report the user can approve + expect(result?.reason).toBe(CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION); + expect(result?.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + it('should return null for an archived report when there is a policy pending join request', async () => { // Given an archived admin room with a pending join request const joinRequestReportAction: ReportAction = {