diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a171c743379b..adfdd5af30a4 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -1154,7 +1154,7 @@ function MoneyRequestView({ const reportNameToDisplay = isFromMergeTransaction ? (updatedTransaction?.reportName ?? translate('common.none')) : getReportName(parentReport, parentReportDerivedName); const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay); const reportCopyValue = !canEditReport && reportNameToDisplay !== translate('common.none') ? reportNameToDisplay : undefined; - const shouldShowCategoryAnalyzing = isCategoryBeingAnalyzed(updatedTransaction ?? transaction); + const shouldShowCategoryAnalyzing = isCategoryBeingAnalyzed(updatedTransaction ?? transaction, policy); // In this case we want to use this value. The shouldUseNarrowLayout will always be true as this case is handled when we display ReportScreen in RHP. // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth diff --git a/src/languages/de.ts b/src/languages/de.ts index 516fbfb24471..054f56b57509 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6190,6 +6190,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU deleteFailureMessage: 'Beim Löschen der Kategorie ist ein Fehler aufgetreten, bitte versuche es erneut.', categoryName: 'Kategoriename', requiresCategory: 'Mitglieder müssen alle Ausgaben kategorisieren', + autoCategorizeNewExpenses: 'Neue Ausgaben automatisch kategorisieren', showCategoryGLCodes: 'Sachkonten beim Kategorisieren von Ausgaben anzeigen', needCategoryForExportToIntegration: (connectionName: string) => `Alle Ausgaben müssen kategorisiert werden, um nach ${connectionName} exportiert zu werden.`, subtitle: 'Verschaffe dir einen besseren Überblick darüber, wofür Geld ausgegeben wird. Verwende unsere Standardkategorien oder füge eigene hinzu.', diff --git a/src/languages/el.ts b/src/languages/el.ts index 1b951f9486e6..59a7b66c8418 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6305,6 +6305,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ deleteFailureMessage: 'Παρουσιάστηκε σφάλμα κατά τη διαγραφή της κατηγορίας, δοκιμάστε ξανά', categoryName: 'Όνομα κατηγορίας', requiresCategory: 'Τα μέλη πρέπει να κατηγοριοποιούν όλες τις δαπάνες', + autoCategorizeNewExpenses: 'Αυτόματη κατηγοριοποίηση νέων δαπανών', showCategoryGLCodes: 'Εμφάνιση κωδικών GL κατά την κατηγοριοποίηση δαπανών', needCategoryForExportToIntegration: (connectionName: string) => `Όλες οι δαπάνες πρέπει να κατηγοριοποιηθούν για να γίνει η εξαγωγή στο ${connectionName}.`, subtitle: 'Αποκτήστε μια καλύτερη εικόνα για το πού ξοδεύονται τα χρήματα. Χρησιμοποιήστε τις προεπιλεγμένες κατηγορίες μας ή προσθέστε τις δικές σας.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 2c31da46b836..00dbacf29b26 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6267,6 +6267,7 @@ const translations = { deleteFailureMessage: 'An error occurred while deleting the category, please try again', categoryName: 'Category name', requiresCategory: 'Members must categorize all expenses', + autoCategorizeNewExpenses: 'Auto-categorize new expenses', showCategoryGLCodes: 'Show GL codes when categorizing expenses', needCategoryForExportToIntegration: (connectionName: string) => `All expenses must be categorized in order to export to ${connectionName}.`, subtitle: 'Get a better overview of where money is being spent. Use our default categories or add your own.', diff --git a/src/languages/es.ts b/src/languages/es.ts index b5c0c4c16b3f..53a508d7967d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6066,6 +6066,7 @@ ${amount} para ${merchant} - ${date}`, deleteFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', categoryName: 'Nombre de la categoría', requiresCategory: 'Los miembros deben clasificar todos los gastos', + autoCategorizeNewExpenses: 'Categorizar automáticamente los gastos nuevos', showCategoryGLCodes: 'Mostrar códigos GL al categorizar gastos', needCategoryForExportToIntegration: (connectionName) => `Todos los gastos deben estar categorizados para poder exportar a ${connectionName}.`, subtitle: 'Obtén una visión general de dónde te gastas el dinero. Utiliza las categorías predeterminadas o añade las tuyas propias.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 30eb674b55ee..0da5956d7c6a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6198,6 +6198,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. deleteFailureMessage: "Une erreur s'est produite lors de la suppression de la catégorie, veuillez réessayer", categoryName: 'Nom de la catégorie', requiresCategory: 'Les membres doivent catégoriser toutes les dépenses', + autoCategorizeNewExpenses: 'Catégoriser automatiquement les nouvelles dépenses', showCategoryGLCodes: 'Afficher les codes de grand livre lors de la catégorisation des dépenses', needCategoryForExportToIntegration: (connectionName: string) => `Toutes les dépenses doivent être catégorisées afin de pouvoir être exportées vers ${connectionName}.`, subtitle: 'Obtenez une meilleure vue d’ensemble de l’endroit où l’argent est dépensé. Utilisez nos catégories par défaut ou ajoutez les vôtres.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 1f1e1cbab42e..d73a8e0eeb0d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6153,6 +6153,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. deleteFailureMessage: 'Si è verificato un errore durante l’eliminazione della categoria, riprova per favore', categoryName: 'Nome categoria', requiresCategory: 'I membri devono categorizzare tutte le spese', + autoCategorizeNewExpenses: 'Categorizza automaticamente le nuove spese', showCategoryGLCodes: 'Mostra i codici CO.GE. quando classifichi le spese', needCategoryForExportToIntegration: (connectionName: string) => `Tutte le spese devono essere categorizzate per poterle esportare su ${connectionName}.`, subtitle: 'Ottieni una panoramica migliore di dove viene speso il denaro. Usa le nostre categorie predefinite oppure aggiungi le tue.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4486e57ea32d..1f58219e1f2b 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6079,6 +6079,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO deleteFailureMessage: 'カテゴリの削除中にエラーが発生しました。もう一度お試しください', categoryName: 'カテゴリ名', requiresCategory: 'メンバーはすべての経費を分類する必要があります', + autoCategorizeNewExpenses: '新しい経費を自動分類する', showCategoryGLCodes: '経費を分類するときに GL コードを表示する', needCategoryForExportToIntegration: (connectionName: string) => `${connectionName} にエクスポートするには、すべての経費にカテゴリを指定する必要があります。`, subtitle: 'お金がどこで使われているかを、より分かりやすく把握しましょう。デフォルトのカテゴリを使うか、自分用のカテゴリを追加できます。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index b89e745993d7..d3892abf1e0c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6145,6 +6145,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ deleteFailureMessage: 'Er is een fout opgetreden bij het verwijderen van de categorie, probeer het opnieuw', categoryName: 'Categorienaam', requiresCategory: 'Leden moeten alle uitgaven categoriseren', + autoCategorizeNewExpenses: 'Nieuwe uitgaven automatisch categoriseren', showCategoryGLCodes: 'Toon GL-codes bij het categoriseren van uitgaven', needCategoryForExportToIntegration: (connectionName: string) => `Alle onkosten moeten worden gecategoriseerd om te kunnen exporteren naar ${connectionName}.`, subtitle: 'Krijg beter inzicht in waar geld wordt uitgegeven. Gebruik onze standaardcategorieën of voeg je eigen categorieën toe.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index bd6821eb7328..fc8365e9da9a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6166,6 +6166,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy deleteFailureMessage: 'Wystąpił błąd podczas usuwania kategorii, spróbuj ponownie', categoryName: 'Nazwa kategorii', requiresCategory: 'Członkowie muszą kategoryzować wszystkie wydatki', + autoCategorizeNewExpenses: 'Automatycznie kategoryzuj nowe wydatki', showCategoryGLCodes: 'Pokaż kody GL podczas kategoryzowania wydatków', needCategoryForExportToIntegration: (connectionName: string) => `Wszystkie wydatki muszą zostać skategoryzowane, aby można je było wyeksportować do ${connectionName}.`, subtitle: 'Uzyskaj lepszy wgląd w to, gdzie wydawane są pieniądze. Użyj naszych domyślnych kategorii lub dodaj własne.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 58e5daad22c5..cd1f3a226967 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6143,6 +6143,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS deleteFailureMessage: 'Ocorreu um erro ao excluir a categoria, tente novamente', categoryName: 'Nome da categoria', requiresCategory: 'Os membros devem categorizar todas as despesas', + autoCategorizeNewExpenses: 'Categorizar automaticamente novas despesas', showCategoryGLCodes: 'Mostrar códigos GL ao categorizar despesas', needCategoryForExportToIntegration: (connectionName: string) => `Todas as despesas devem ser categorizadas para serem exportadas para ${connectionName}.`, subtitle: 'Tenha uma visão melhor de onde o dinheiro está sendo gasto. Use nossas categorias padrão ou adicione as suas.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c3d39466111a..8f002992e3fd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5936,6 +5936,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM deleteFailureMessage: '删除类别时出错,请重试', categoryName: '类别名称', requiresCategory: '成员必须为所有报销分类', + autoCategorizeNewExpenses: '自动为新报销分类', showCategoryGLCodes: '在分类报销时显示总账科目代码', needCategoryForExportToIntegration: (connectionName: string) => `要导出到 ${connectionName},所有报销都必须先进行分类。`, subtitle: '更好地了解资金的支出去向。使用我们的默认类别或添加你自己的类别。', diff --git a/src/libs/API/parameters/SetPolicyAutoCategorizeNewExpensesParams.ts b/src/libs/API/parameters/SetPolicyAutoCategorizeNewExpensesParams.ts new file mode 100644 index 000000000000..058a0b6d6f13 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyAutoCategorizeNewExpensesParams.ts @@ -0,0 +1,6 @@ +type SetPolicyAutoCategorizeNewExpensesParams = { + policyID: string; + enabled: boolean; +}; + +export default SetPolicyAutoCategorizeNewExpensesParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 24e6cfd7d0af..c5c69372901a 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -261,6 +261,7 @@ export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspac export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams'; export type {default as RenameWorkspaceCategoriesParams} from './RenameWorkspaceCategoriesParams'; export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams'; +export type {default as SetPolicyAutoCategorizeNewExpensesParams} from './SetPolicyAutoCategorizeNewExpensesParams'; export type {default as SetPolicyShowCategoryGLCodesParams} from './SetPolicyShowCategoryGLCodesParams'; export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams'; export type {default as UpdatePolicyCategoryPayrollCodeParams} from './UpdatePolicyCategoryPayrollCodeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 6b8fac5d492e..7aa4502eab92 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -189,6 +189,7 @@ const WRITE_COMMANDS = { CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', + SET_POLICY_AUTO_CATEGORIZE_NEW_EXPENSES: 'SetPolicyAutoCategorizeNewExpenses', SET_POLICY_SHOW_CATEGORY_GL_CODES: 'SetPolicyShowCategoryGLCodes', UPDATE_POLICY_CATEGORY_PAYROLL_CODE: 'UpdatePolicyCategoryPayrollCode', UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode', @@ -847,6 +848,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.EXPORT_RECEIPTS_TO_ZIP]: Parameters.ExportReceiptsToZipParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; + [WRITE_COMMANDS.SET_POLICY_AUTO_CATEGORIZE_NEW_EXPENSES]: Parameters.SetPolicyAutoCategorizeNewExpensesParams; [WRITE_COMMANDS.SET_POLICY_SHOW_CATEGORY_GL_CODES]: Parameters.SetPolicyShowCategoryGLCodesParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE]: Parameters.UpdatePolicyCategoryPayrollCodeParams; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 204babcaddbd..47f42c1f72e3 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1795,12 +1795,19 @@ function isReceiptBeingScanned(transaction: OnyxInputOrEntry): bool /** * Check if category is being analyzed (manual request creation or auto-categorization grace period) + * + * @param transaction - The transaction whose category may still be auto-categorized + * @param policy - The workspace policy; auto-categorize defaults to on when the attribute is unset */ -function isCategoryBeingAnalyzed(transaction: OnyxEntry): boolean { +function isCategoryBeingAnalyzed(transaction: OnyxEntry, policy?: OnyxEntry): boolean { if (!transaction) { return false; } + if (policy?.autoCategorizeNewExpenses === false) { + return false; + } + if (isExpenseUnreported(transaction)) { return false; } @@ -2095,7 +2102,7 @@ function shouldShowViolation( return isAttendeeTrackingEnabledForPolicy(policy); } - if (violationName === CONST.VIOLATIONS.MISSING_CATEGORY && isCategoryBeingAnalyzed(transaction)) { + if (violationName === CONST.VIOLATIONS.MISSING_CATEGORY && isCategoryBeingAnalyzed(transaction, policy)) { return false; } diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 97108c62f66c..da158ab8f7f5 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1284,6 +1284,62 @@ function setWorkspaceRequiresCategory(policyData: PolicyData, requiresCategory: API.write(WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY, parameters, onyxData); } +function setPolicyAutoCategorizeNewExpenses(policyID: string, enabled: boolean) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoCategorizeNewExpenses: enabled, + errorFields: { + autoCategorizeNewExpenses: null, + }, + pendingFields: { + autoCategorizeNewExpenses: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + errorFields: { + autoCategorizeNewExpenses: null, + }, + pendingFields: { + autoCategorizeNewExpenses: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoCategorizeNewExpenses: !enabled, + errorFields: { + autoCategorizeNewExpenses: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), + }, + pendingFields: { + autoCategorizeNewExpenses: null, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + enabled, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_AUTO_CATEGORIZE_NEW_EXPENSES, parameters, onyxData); +} + function setPolicyShowCategoryGLCodes(policyID: string | undefined, showCategoryGLCodes: boolean) { if (!policyID) { return; @@ -2004,5 +2060,6 @@ export { setWorkspaceCategoryDescriptionHint, setWorkspaceCategoryEnabled, setWorkspaceRequiresCategory, + setPolicyAutoCategorizeNewExpenses, setPolicyShowCategoryGLCodes, }; diff --git a/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx index 3891116b7ec4..364c52ce991b 100644 --- a/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/DynamicWorkspaceCategoriesSettingsPage.tsx @@ -24,7 +24,7 @@ import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnec import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {setPolicyShowCategoryGLCodes, setWorkspaceRequiresCategory} from '@userActions/Policy/Category'; +import {setPolicyAutoCategorizeNewExpenses, setPolicyShowCategoryGLCodes, setWorkspaceRequiresCategory} from '@userActions/Policy/Category'; import {clearPolicyErrorField} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -60,6 +60,13 @@ function DynamicWorkspaceCategoriesSettingsPage({policy, route}: DynamicWorkspac [policyData], ); + const updateAutoCategorizeNewExpenses = useCallback( + (value: boolean) => { + setPolicyAutoCategorizeNewExpenses(policyID, value); + }, + [policyID], + ); + const updateShowCategoryGLCodes = (value: boolean) => { setPolicyShowCategoryGLCodes(policyID, value); }; @@ -83,9 +90,6 @@ function DynamicWorkspaceCategoriesSettingsPage({policy, route}: DynamicWorkspac const hasEnabledCategories = hasEnabledOptions(policyData.categories); const isToggleDisabled = !policy?.areCategoriesEnabled || !hasEnabledCategories || isConnectedToAccounting; - // Under the revamp only the GL codes toggle is left here, so the page has nothing to show without it. - const shouldBlockEmptySettings = isRulesRevampEnabled && !policy?.glCodes; - const onSelectItem = (item: ListItem) => { if (!item.groupID) { return; @@ -111,7 +115,6 @@ function DynamicWorkspaceCategoriesSettingsPage({policy, route}: DynamicWorkspac policyID={policyID} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} - shouldBeBlocked={shouldBlockEmptySettings} > )} + clearPolicyErrorField(policy?.id, 'autoCategorizeNewExpenses')} + /> {!!policy?.glCodes && ( { expect(TransactionUtils.isCategoryBeingAnalyzed(transaction)).toBe(true); }); + it('should return false when auto-categorize new expenses is disabled on the policy', () => { + const transaction = generateTransaction({ + category: '', + merchant: 'Some Merchant', + amount: 100, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }); + const policy = {...createRandomPolicy(0), autoCategorizeNewExpenses: false}; + + expect(TransactionUtils.isCategoryBeingAnalyzed(transaction, policy)).toBe(false); + }); + + it('should return true when auto-categorize new expenses is enabled on the policy', () => { + const transaction = generateTransaction({ + category: '', + merchant: 'Some Merchant', + amount: 100, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }); + const policy = {...createRandomPolicy(0), autoCategorizeNewExpenses: true}; + + expect(TransactionUtils.isCategoryBeingAnalyzed(transaction, policy)).toBe(true); + }); + it('should return true when within auto-categorization grace period', () => { // Set pendingAutoCategorizationTime to 30 seconds ago (within 1 minute grace period) const thirtySecondsAgo = new Date(Date.now() - 30 * 1000); @@ -2359,6 +2383,22 @@ describe('TransactionUtils', () => { expect(TransactionUtils.isCategoryBeingAnalyzed(transaction)).toBe(true); }); + it('should return false during the grace period when auto-categorize new expenses is disabled', () => { + const thirtySecondsAgo = new Date(Date.now() - 30 * 1000); + const pendingAutoCategorizationTime = thirtySecondsAgo.toISOString().replace('T', ' ').replace('Z', ''); + const transaction = generateTransaction({ + category: '', + merchant: 'Some Merchant', + amount: 100, + comment: { + pendingAutoCategorizationTime, + }, + }); + const policy = {...createRandomPolicy(0), autoCategorizeNewExpenses: false}; + + expect(TransactionUtils.isCategoryBeingAnalyzed(transaction, policy)).toBe(false); + }); + it('should return false when auto-categorization grace period has passed', () => { // Set pendingAutoCategorizationTime to 2 minutes ago (outside 1 minute grace period) const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);