You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-28] [$500] [Refactor] Refactor PolicyUtils.ts to stay under the 4000-line ESLint limit #102675
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.
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
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
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)
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.
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.
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.
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.
connections.ts, netsuite.ts, sageIntacct.ts, xero.ts. All leaves. ~400 lines together, or one PR per integration if that reviews more easily.
customUnits.ts and tax.ts.tax.ts imports getDistanceRateCustomUnit from ./customUnits.
rules.ts, features.ts, workflows.ts, errors.ts. These sit on policyType and connections, which already exist.
approvals.ts and payments.ts. Riskiest call graph (getSubmitToEmail, rule matching). Do it only after workflows, permissions, and tags are real modules.
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.
⏱️ 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.
src/libs/PolicyUtils.ts is still one file. A sibling tags.ts cannot sit next to it until the module is a directory.
Move src/libs/PolicyUtils.ts to src/libs/PolicyUtils/index.ts.
Add src/libs/PolicyUtils/tags.ts.
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.
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.
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 👍
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
Activity
melvin-bot commentedon Sep 30, 2026
Current assignee @nkdengineer is eligible for the External assigner, not assigning anyone new.
[-][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[/+]melvin-bot commentedon Sep 30, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105327754295937031
nkdengineer commentedon Sep 30, 2026
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.
types.tsMemberEmailsToAccountIDs,PolicyFeature,PolicyFeatureAccess, plus types used by more than one module (PolicyWithTaxRates,AccountingConnectionName,WorkspaceDetails,ApprovalWorkflowContext,ApprovalWorkflowRuleMatch,TravelStep)policyType.tscanPolicyAccessFeature,isSubmitterApproveBlockedOnSubmitWorkspacetypespermissions.tsisPolicyAdmin,getPolicyRole, editor / auditor / user / employee), feature-permission bundles,canMember*,canEditWorkspaceSettings,isPolicyApprover,getPolicyApproverLoginspolicyTypetags.tsgetTagApproverRulerules.tshasCustomCategories,isMaxExpenseAmountSet,hasConfiguredRules,getRulesDocumentSourceURLcustomUnits.tsgetCustomUnitsForDuplication,cloneCustomUnitWithNewIDs)policyTypeconnections.tsisPreferredExporter, Uber receipt-partner errorvendors.tsnetsuite.tssageIntacct.tsxero.tshasPolicyWithXeroConnectiontax.tsisTaxTrackingEnabled, tax-rate maps,canDisableOrDeleteTaxRate,getCurrentTaxID,resolveCurrentTaxCode,isPolicyTaxEnabled,isTaxCodeCustomizedcustomUnitsfeatures.tsisPolicyFeatureEnabledand the feature checks it fans out to (arePolicyRulesEnabled, invoice fields, attendee tracking, time tracking, MCP)policyType,connectionsworkflows.tsisSubmitAndClose, dynamic external workflowconnectionserrors.tsareSettingsInErrorFields,settingsPendingActionpermissions,connectionspayments.tsPAYER_ROLES,canRolePay,isPolicyPayer, reimbursement choice, payments enabled, auto-pay availabilitypermissionsapprovals.tsgetWorkflowApprovalsUnavailableworkflows,policyType,permissions,tagsworkspaces.tsshouldShowPolicy, owned / group / chat-enabled policies, admin and employee workspaces, plan type, deletion guard, sort helpers, and the three invoice helpers (canSendInvoiceFromWorkspace,canSubmitPerDiemExpenseFromWorkspace,canSendInvoice)policyType,permissions,customUnits,featuresmembers.tsisDeletedPolicyEmployee, and bank-account share recipientspermissions,workspacesdomains.tsmemberstravel.tsgetTravelSteppolicyType,workspaces,domainsnavigation.tstryNavigateToSubmitWorkspaceUpgrade,tryNavigateToControlPolicyUpgrade,goBackFromInvalidPolicy,goBackWhenEnableFeature,navigateToExpensifyCardPagepolicyTypeindex.tsgetAllTaxRatesNamesAndKeys as getAllTaxRatesandexport type {MemberEmailsToAccountIDs, PolicyFeature, PolicyFeatureAccess}index.tsat ~300 lines of re-exports is the public API. That is the point: callers andjest.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,getFirstRuleApproverand the approval-filter matchers,getAccountingConnectionNames,isAccountingConnectionName, the per-integrationis*VendorMatchingActivechecks that are not already exported,getActiveVendorMatchingVendors, andhasUnsupportedIntegration.Suggested PR order
One PR that touches all 4039 lines will conflict with every open policy PR. Split it. Each step leaves
index.tsre-exporting the moved names, so no importer changes.tags.ts. Leaf, ~350 lines. This alone dropsindex.tsunder 4000 and clears the lint failure.getRuleApprovers(still in the old file) importsgetTagApproverRulefrom./tags, not the other way around.vendors.ts. Largest leaf, ~490 lines. The mention ofgetSageIntacctVendorsinside it is a comment, not a call, so it does not needsageIntacct.tsyet.policyType.ts, thenpermissions.ts.policyType.tsis a leaf. Extract it first in this PR, then movepermissions.tsso it imports./policyType. Do not extractpermissions.tswhileisControlPolicy/isArchivedPolicy/isSubmitPolicystill live inindex.ts.connections.ts,netsuite.ts,sageIntacct.ts,xero.ts. All leaves. ~400 lines together, or one PR per integration if that reviews more easily.customUnits.tsandtax.ts.tax.tsimportsgetDistanceRateCustomUnitfrom./customUnits.rules.ts,features.ts,workflows.ts,errors.ts. These sit onpolicyTypeandconnections, which already exist.approvals.tsandpayments.ts. Riskiest call graph (getSubmitToEmail, rule matching). Do it only afterworkflows,permissions, andtagsare real modules.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.tsbecomes the barrel.nkdengineer commentedon Sep 30, 2026
@MelvinBot Here is the split-file plan for
PolicyUtils. Let's implement the first PR based onSuggested PR ordersection.MelvinBot commentedon Sep 30, 2026
⏱️ 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
camesenin commentedon Sep 30, 2026
melvin-bot commentedon Sep 30, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
nkdengineer commentedon Oct 1, 2026
Turn the file into a folder
src/libs/PolicyUtils.tsis still one file. A siblingtags.tscannot sit next to it until the module is a directory.src/libs/PolicyUtils.tstosrc/libs/PolicyUtils/index.ts.src/libs/PolicyUtils/tags.ts.import ... from '@libs/PolicyUtils'andfrom './PolicyUtils'as it is. Both resolve toindex.ts.index.tskeeps every current export, includinggetAllTaxRatesNamesAndKeys as getAllTaxRates. Callers andjest.mock('@libs/PolicyUtils')stay on that barrel. Nothing outside this folder should import@libs/PolicyUtils/tags.What moves into
tags.tsCopy these functions unchanged. They only call each other,
CONST,isEmptyObject,Str.htmlDecode, andgetTagArrayFromNamefrom@libs/TransactionUtils.getSortedTagKeys,getTagListName,getTagLists,hasTags,matchesParentTagsFilter,matchesParentTagPath,findPolicyTagAtLevel,findPolicyTagEntryByParentFilter,isTagInPolicygetTagList,getTagListByOrderWeight,getTagNamesFromTagsLists,getCleanedTagName,getCommaSeparatedTagNameWithSanitizedColons,getLengthOfTag,getTagGLCode,getGLCodeFromPolicyTag,escapeTagName,isDefaultTagName,getCountOfEnabledTagsOfList,getCountOfRequiredTagLists,isMultiLevelTagshasDependentTags,hasIndependentTags,hasPerTagListRequired,getTagListLabelgetTagApproverRuleKeep
LITERAL_PARENT_TAGS_FILTERandESCAPED_CHARACTERprivate intags.ts. They are not exported today.Leave the block between the first two tag sections in
index.ts:hasCustomCategories,isMaxExpenseAmountSet, andhasConfiguredRules(1306-1398). Those belong torules.tsin a later PR.Start
tags.tswith a short file-header comment describing tag-list lookup, parent-tag matching, and GL codes. Export the 27 functions above.index.tsimports them from./tagsand keeps their names in the existingexport { ... }block.The one edge back into
index.tsgetRuleApprovers(line 2016) stays inindex.tsand callsgetTagApproverRule. After the move, that call comes from the./tagsimport.tags.tsdoes not importindex.ts, so there is no cycle and noimport/no-cycledisable.index.tsshould land around 3450 lines, under the 4000-linemax-linescap.94 remaining items
blimpich commentedon Oct 22, 2026
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 👍
[-][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[/+]melvin-bot commentedon Oct 22, 2026
Upwork job price has been updated to $500
nkdengineer commentedon Oct 22, 2026
Thanks. I noted that.
melvin-bot commentedon Oct 27, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 27, 2026
Payment Summary
Resolving PRs:
[Refactor] Move PolicyUtils into a folder and split tag helpers into tags.ts #102724
[Refactor] Move PolicyUtils vendor-matching helpers into vendor.ts #103135
[Refactor] Move PolicyUtils policy-type and permission helpers into policyType.ts and permissions.ts #103281
[Refactor] Move PolicyUtils connection, Xero, and Sage Intacct helpers into connections.ts, xero.ts, and sageIntacct.ts #103546
[Refactor] Move PolicyUtils custom unit and tax helpers into customUnits.ts and tax.ts #103820
[Refactor] Move PolicyUtils rules, feature, workflow, and error helpers into rules.ts, features.ts, workflows.ts, and errors.ts #103958
[Refactor] Move PolicyUtils approval and payment helpers into approvals.ts and payments.ts #104059
[Refactor] Move PolicyUtils workspace and navigation helpers into policy.ts and navigation.ts #104183
[Refactor] Move remaining PolicyUtils member, domain, and travel helpers into members.ts, domains.ts, and travel.ts #104331
Reviewer: @nkdengineer owed $500 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 30, 2026
Payment Summary
Contributor+: @nkdengineer due $500 via NewDot
twisterdotcom commentedon Oct 30, 2026
$500 approved for @nkdengineer in Report ID: R00XQOeQWj6k.
Zapier Logs
Run ID: 00040eee-f7ba-afc4-46a4-ee6398cb1c84SO: https://stackoverflowteams.com/c/expensify/questions/7582