Skip to content

[Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit #102675

Closed

Description

@blimpich

Coming from here, PolicyUtils.ts has gotten close to it's max size, which leads to preventable issues where we go over the limit and then suddenly the build is broken, blocking deploys and other people's PRs.

Solution

Let's break up the file as per @nkdengineer's plan here.

Issue OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 30, 2026
  2. melvin-bot commented on Sep 30, 2026

    @melvin-bot

    Current assignee @nkdengineer is eligible for the External assigner, not assigning anyone new.

  3. changed the title [-][Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-] [+][$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+] on Sep 30, 2026
  4. melvin-bot commented on Sep 30, 2026

    @melvin-bot
  5. nkdengineer commented on Sep 30, 2026

    @nkdengineer
    Contributor

    Split plan

    Sizes are the current function bodies, not a promise about the formatted result. Scattered helpers are called out so a move is not "cut this line range" when the functions are not contiguous.

    New file What moves Where it is now ~Size Imports
    types.ts Exported types MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess, plus types used by more than one module (PolicyWithTaxRates, AccountingConnectionName, WorkspaceDetails, ApprovalWorkflowContext, ApprovalWorkflowRuleMatch, TravelStep) 72-111 ~40 nothing
    policyType.ts Archived / pending-delete checks, paid vs Submit vs group vs Control vs Collect, canPolicyAccessFeature, isSubmitterApproveBlockedOnSubmitWorkspace 115-139, 1544-1585, 1615-1622, 1627-1635, 1659-1689 ~120 types
    permissions.ts Role checks (isPolicyAdmin, getPolicyRole, editor / auditor / user / employee), feature-permission bundles, canMember*, canEditWorkspaceSettings, isPolicyApprover, getPolicyApproverLogins 187-336, 689-713, 731-747, 821-848, 909-931, 1586-1614 ~275 policyType
    tags.ts Tag lists, parent-tag matching, GL code, dependent / independent tags, getTagApproverRule 1167-1308, 1403-1543, 2387-2440, 3394-3406 ~350 nothing
    rules.ts hasCustomCategories, isMaxExpenseAmountSet, hasConfiguredRules, getRulesDocumentSourceURL 1309-1402, 3752-3774 ~120 nothing
    customUnits.ts Distance and per diem units, rate display, duplication (getCustomUnitsForDuplication, cloneCustomUnitWithNewIDs) 398-482, 528-663, 2799-2807 ~230 policyType
    connections.ts Current connection, auto-sync, unsupported / OldDot-only integrations, last successful sync, exporters, isPreferredExporter, Uber receipt-partner error 850-854, 1840-1847, 2685-2711, 2820-2835, 3326-3356, 3650-3656, 3699-3714 ~110 nothing
    vendors.ts Vendor matching for every integration, vendor display / empty state, Xero suppliers, Campfire / DualEntry / Certinia vendor lists 2836-3325 ~490 nothing
    netsuite.ts NetSuite account, vendor, tax, and custom-field option builders 2495-2684 ~190 nothing
    sageIntacct.ts Sage Intacct entity, bank, expense, vendor, and credit-card options 2712-2777 ~65 nothing
    xero.ts Xero tenants, org name, bank and expense accounts, hasPolicyWithXeroConnection 2367-2371, 2441-2473 ~40 nothing
    tax.ts isTaxTrackingEnabled, tax-rate maps, canDisableOrDeleteTaxRate, getCurrentTaxID, resolveCurrentTaxCode, isPolicyTaxEnabled, isTaxCodeCustomized 1699-1720, 1853-1910, 3365-3393, 3731-3739, 3775 ~130 customUnits
    features.ts isPolicyFeatureEnabled and the feature checks it fans out to (arePolicyRulesEnabled, invoice fields, attendee tracking, time tracking, MCP) 1650-1658, 1911-1969, 3715-3730 ~85 policyType, connections
    workflows.ts Instant / delayed submit, auto-reporting frequency, isSubmitAndClose, dynamic external workflow 1721-1766, 1821-1839 ~65 connections
    errors.ts Policy error flags, brick-road status, the two selector wrappers at 682 and 687, areSettingsInErrorFields, settingsPendingAction 338-396, 665-687, 2474-2494 ~105 permissions, connections
    payments.ts Reimburser, PAYER_ROLES, canRolePay, isPolicyPayer, reimbursement choice, payments enabled, auto-pay availability 748-820, 1767-1791 ~100 permissions
    approvals.ts Approval mode, default approver, custom workflows, rule matching, submit-to / forwards-to, getWorkflowApprovalsUnavailable 1792-1820, 1971-2334, 3415-3418 ~400 workflows, policyType, permissions, tags
    workspaces.ts Active-policy filters, shouldShowPolicy, owned / group / chat-enabled policies, admin and employee workspaces, plan type, deletion guard, sort helpers, and the three invoice helpers (canSendInvoiceFromWorkspace, canSubmitPerDiemExpenseFromWorkspace, canSendInvoice) 141-186, 714-730, 1623-1626, 2335-2366, 2372-2386, 2778-2798, 3361-3364, 3419-3579, 3740-3751 ~310 policyType, permissions, customUnits, features
    members.ts Expensify-team filtering, invite exclusions, guides and account managers, workspace member account IDs, isDeletedPolicyEmployee, and bank-account share recipients 483-527, 856-908, 932-1166, 3357-3360 ~340 permissions, workspaces
    domains.ts Policy domain name, admin private domains, most frequent domain, domain-card description 3407-3414, 3580-3649 ~80 members
    travel.ts Travel provisioning, accepted terms, non-USD, getTravelStep 3657-3698 ~40 policyType, workspaces, domains
    navigation.ts tryNavigateToSubmitWorkspaceUpgrade, tryNavigateToControlPolicyUpgrade, goBackFromInvalidPolicy, goBackWhenEnableFeature, navigateToExpensifyCardPage 1636-1649, 1690-1698, 1848-1852, 2808-2819 ~40 policyType
    index.ts Re-exports only, including the existing alias getAllTaxRatesNamesAndKeys as getAllTaxRates and export type {MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess} 3784-4039 ~300 the modules above

    index.ts at ~300 lines of re-exports is the public API. That is the point: callers and jest.mock('@libs/PolicyUtils') stay on the barrel.

    Internal-only helpers stay private to their module. They are not in the export block today: the feature-permission maps, isControlPolicyOnlyRole, hasPolicyFeaturePermission, shouldShowPolicyErrorFields, getNumericValue, getDefaultDistanceRate, cloneCustomUnitWithNewIDs, the parent-tag regexes, isSubmitPolicyByType, isPolicyEditor, isControlOnAdvancedApprovalMode, getFirstRuleApprover and the approval-filter matchers, getAccountingConnectionNames, isAccountingConnectionName, the per-integration is*VendorMatchingActive checks that are not already exported, getActiveVendorMatchingVendors, and hasUnsupportedIntegration.

    Suggested PR order

    One PR that touches all 4039 lines will conflict with every open policy PR. Split it. Each step leaves index.ts re-exporting the moved names, so no importer changes.

    1. tags.ts. Leaf, ~350 lines. This alone drops index.ts under 4000 and clears the lint failure. getRuleApprovers (still in the old file) imports getTagApproverRule from ./tags, not the other way around.
    2. vendors.ts. Largest leaf, ~490 lines. The mention of getSageIntacctVendors inside it is a comment, not a call, so it does not need sageIntacct.ts yet.
    3. policyType.ts, then permissions.ts. policyType.ts is a leaf. Extract it first in this PR, then move permissions.ts so it imports ./policyType. Do not extract permissions.ts while isControlPolicy / isArchivedPolicy / isSubmitPolicy still live in index.ts.
    4. connections.ts, netsuite.ts, sageIntacct.ts, xero.ts. All leaves. ~400 lines together, or one PR per integration if that reviews more easily.
    5. customUnits.ts and tax.ts. tax.ts imports getDistanceRateCustomUnit from ./customUnits.
    6. rules.ts, features.ts, workflows.ts, errors.ts. These sit on policyType and connections, which already exist.
    7. approvals.ts and payments.ts. Riskiest call graph (getSubmitToEmail, rule matching). Do it only after workflows, permissions, and tags are real modules.
    8. workspaces.ts (including the three invoice helpers), members.ts (including bank-account sharing), domains.ts, travel.ts, navigation.ts. This empties the old file. index.ts becomes the barrel.
  6. nkdengineer commented on Sep 30, 2026

    @nkdengineer
    Contributor

    @MelvinBot Here is the split-file plan for PolicyUtils. Let's implement the first PR based on Suggested PR order section.

  7. MelvinBot commented on Sep 30, 2026

    @MelvinBot
    Contributor

    ⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.

    Run details

  8. camesenin commented on Sep 30, 2026

    @camesenin
  9. melvin-bot commented on Sep 30, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. nkdengineer commented on Oct 1, 2026

    @nkdengineer
    Contributor

    Turn the file into a folder

    src/libs/PolicyUtils.ts is still one file. A sibling tags.ts cannot sit next to it until the module is a directory.

    1. Move src/libs/PolicyUtils.ts to src/libs/PolicyUtils/index.ts.
    2. Add src/libs/PolicyUtils/tags.ts.
    3. Leave every existing import ... from '@libs/PolicyUtils' and from './PolicyUtils' as it is. Both resolve to index.ts.

    index.ts keeps every current export, including getAllTaxRatesNamesAndKeys as getAllTaxRates. Callers and jest.mock('@libs/PolicyUtils') stay on that barrel. Nothing outside this folder should import @libs/PolicyUtils/tags.

    What moves into tags.ts

    Copy these functions unchanged. They only call each other, CONST, isEmptyObject, Str.htmlDecode, and getTagArrayFromName from @libs/TransactionUtils.

    Block Lines now Functions
    Tag lists and parent matching 1167-1304 getSortedTagKeys, getTagListName, getTagLists, hasTags, matchesParentTagsFilter, matchesParentTagPath, findPolicyTagAtLevel, findPolicyTagEntryByParentFilter, isTagInPolicy
    Tag name, GL code, counts 1400-1542 getTagList, getTagListByOrderWeight, getTagNamesFromTagsLists, getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getLengthOfTag, getTagGLCode, getGLCodeFromPolicyTag, escapeTagName, isDefaultTagName, getCountOfEnabledTagsOfList, getCountOfRequiredTagLists, isMultiLevelTags
    Dependent tags 2387-2438 hasDependentTags, hasIndependentTags, hasPerTagListRequired, getTagListLabel
    Tag approver rule 3204-3215 getTagApproverRule

    Keep LITERAL_PARENT_TAGS_FILTER and ESCAPED_CHARACTER private in tags.ts. They are not exported today.

    Leave the block between the first two tag sections in index.ts: hasCustomCategories, isMaxExpenseAmountSet, and hasConfiguredRules (1306-1398). Those belong to rules.ts in a later PR.

    Start tags.ts with a short file-header comment describing tag-list lookup, parent-tag matching, and GL codes. Export the 27 functions above. index.ts imports them from ./tags and keeps their names in the existing export { ... } block.

    The one edge back into index.ts

    getRuleApprovers (line 2016) stays in index.ts and calls getTagApproverRule. After the move, that call comes from the ./tags import. tags.ts does not import index.ts, so there is no cycle and no import/no-cycle disable.

    index.ts should land around 3450 lines, under the 4000-line max-lines cap.

  11. 94 remaining items

  12. blimpich commented on Oct 22, 2026

    @blimpich
    ContributorAuthor

    I think $500 is the max I would pay out for this. I appreciate the work but I would encourage you to seek a higher payout earlier in the process rather than at the end if it's going to be that big of an increase 👍

  13. changed the title [-][Due for payment 2026-10-28] [$175] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/-] [+][Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit[/+] on Oct 22, 2026
  14. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    Upwork job price has been updated to $500

  15. nkdengineer commented on Oct 22, 2026

    @nkdengineer
    Contributor

    Thanks. I noted that.

  16. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  17. mallenexpensify commented on Oct 30, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @nkdengineer due $500 via NewDot

  18. twisterdotcom commented on Oct 30, 2026

    @twisterdotcom
    Contributor

    $500 approved for @nkdengineer in Report ID: R00XQOeQWj6k.

    Zapier Logs Run ID: 00040eee-f7ba-afc4-46a4-ee6398cb1c84

    SO: https://stackoverflowteams.com/c/expensify/questions/7582

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions