Add primary-only flag to service modifiers

Objective

  • Allow service modifiers to optionally run only when that service's charge line is the primary service on the job
  • Prevent modifier stacking when the same modifier type (e.g. Requested Inspector) is configured on multiple services on one inspection
  • Give companies a first-class setting instead of workarounds like attaching a modifier only to the home inspection service

Background

  • Modifiers are evaluated per service/charge line during scheduling, quote pricing, workorder recalculation, and payroll admin/flat-rate adjustments
  • The Requested Inspector modifier uses a job-level flag (_inspectorRequestedIds / inspector-request star) but applies independently to every service on the job that has the modifier configured — so a $75 requested fee on Home Inspection and a $25 fee on Radon both fire on the same job
  • isPrimary is already computed per charge line in pricing (calculateServicePrices, recalculateChargesForRequiredInfo) and passed into modifierPriceAdjFn, but individual modifiers do not currently gate on it
  • A similar primary only pattern already exists elsewhere in service settings (required reports and agreement templates use primaryOnly with tooltip copy: "Only triggers if the service is a primary service")

Product Decisions

Locked

  1. SemanticsprimaryOnly means: skip the modifier when this service's charge line is not primary on the job. It is not a "once per job" deduplication flag — if Radon is booked standalone and is primary, a Radon modifier with primaryOnly still applies
  2. Default — Existing modifiers behave as today (primaryOnly false / unset). No automatic migration of live modifier configs
  3. Immediate use case — Requested Inspector fee should be configurable to apply only on the primary service line, avoiding duplicate upcharges when add-ons share the job
  4. Modifier type scopeprimaryOnly is available on all modifier types (increment, bracket, compare, enum, zipcodes, requested, conditional)
  5. UI placement — Engineering chooses shared vs per-form placement, as long as the modifier settings UI stays consistent and the new toggle is accommodated cleanly; tooltip copy should match existing primaryOnly language on required reports ("Only triggers if the service is a primary service")

Open

None — all product decisions resolved.

Scope

Backend (attik-backend)

  • src/models/modifierSchema.ts — add optional primaryOnly on modiferData; CRUD via src/routes/modifier.ts
  • src/util/functions/schedule/modifierPriceCalc.tscalculateModifierPrice must accept isPrimary (or equivalent) and skip modifiers when primaryOnly is true and the line is not primary; apply gating for all modifier types; mirror existing addType: 'skip' pattern for missing required-info fields
  • src/util/functions/payroll/payrollContext.tsapplyModifiersToInspectionsBatch recalculates modifiers per charge but does not pass isPrimary today; wire charge.isPrimary so payroll respects the flag
  • src/routes/aiEndpoints.ts — any scheduler/AI pricing paths calling calculateModifierPrice should pass primary context consistently

Frontend (attik-frontend)

  • src/util/types/serverTypeCollection/services.ts — extend ModifierSchema / modiferData with primaryOnly
  • src/util/functions/data/modifierPriceAdjFn.ts — gate all modifier types using service.isPrimary (already on the function signature)
  • src/util/functions/schedulingHelpers/calculateServicePrices.ts and recalculateChargesForRequiredInfo.ts — already pass isPrimary; verify no regressions once gating is added
  • Service settings UI under src/app/tools/settings/services/[id]/Modifier/ — add primaryOnly toggle on all modifier forms (shared via ModifierHeader.tsx or equivalent consistent pattern); follow ServiceRequiredReportsModal.tsx for tooltip copy

Out of scope (unless expanded)

  • Job-level "apply this modifier once total" deduplication across different services
  • Changing how primary service is assigned (canBePrimary, first eligible service in usePriceCalculation)

References

  • Modifier pricing (frontend): src/util/functions/data/modifierPriceAdjFn.ts, src/util/functions/schedulingHelpers/calculateServicePrices.ts
  • Modifier pricing (backend): src/util/functions/schedule/modifierPriceCalc.ts
  • Requested inspector UI: src/app/tools/settings/services/[id]/Modifier/Requested.tsx
  • Existing primaryOnly precedent: src/app/tools/settings/services/[id]/components/ServiceRequiredReportsModal.tsx

Please authenticate to join the conversation.

Upvoters
Status

Planned

Board
🏠

Main App

Date

About 2 hours ago

Author

Linear

Subscribe to post

Get notified by email when there are changes.