Skip to content

Field-level workflow parity audit β€” web ↔ Flutter ​

Date: 2026-07-14. Phase 3 of the input-parity program (Phase 1 shipped labels, Phase 2 shipped option-sets β€” both merged). This phase compares behavior, not vocabulary: field presence, conditional visibility, required/defaults, computed/derived writes, and validation bounds, per entity form.

Audited by 7 parallel read-only research passes (one per entity). transactions was excluded: it's a MoJ-scraped, import/display-only table with no user-editable form on either side, so "workflow parity" doesn't apply.

Update (same day): 9 of the 11 priority findings shipped as fixes β€” web PR aldilaijanre#1078, Flutter PR #358. One finding (#11) turned out to be a false positive on closer verification against the live DB β€” see its entry below. #9 and several secondary items remain open pending a product decision (not one-line bug fixes).

How to read this ​

Each finding is tagged:

  • BUG β€” a real correctness/data-integrity risk, not an intentional platform difference.
  • DRIFT β€” same field, different behavior; may or may not need fixing.
  • WEB-ONLY / FLUTTER-ONLY (documented) β€” the source code itself documents this as deliberate mobile-scope reduction or desktop-only richness. Not a bug.

Priority findings (real bugs, cross-checked against source) ​

  1. βœ… FIXED (Flutter #358) β€” Interactions β€” Flutter never creates the auto-follow-up reminder. Web's useCreateInteraction inserts a reminders row whenever outcome is a needs-follow-up value and a follow_up_date is set (stamping auto_reminder_created). Flutter's InteractionsRepository.create() has no equivalent logic at all β€” an interaction logged from mobile with "needs follow up" silently loses the reminder that logging the same thing from web would have created. Highest-confidence, highest-impact finding across the whole audit.

  2. βœ… FIXED (Flutter #358) β€” Clients β€” Flutter's duplicate-phone handling can silently redirect into the wrong client. Web's find_client_phone_duplicates RPC pre-checks and shows an explicit alert before submit. Flutter has no pre-check; instead ClientsRepository.create() catches ANY Postgres 23505 (unique violation) and regex-extracts a UUID from the error detail, treating it as "this client already exists, navigate there" β€” which also matches a genuine phone-collision-with-a-different-person, silently taking the user to an unrelated existing client's record with a success toast. Verified against the live enforce_client_phone_uniqueness trigger body: it matches purely by normalized phone within the same system, with no notion of "same party" at all.

  3. βœ… FIXED (web #1078) β€” Clients β€” entity_type has no web control at all. The DB column and Flutter's full company/government/bank support exist, but web's client_create form_fields never seed an entity_type control β€” every web-created client is silently pinned to 'individual'. Since web is likely the higher-volume creation surface, this probably means most non-individual clients are currently misclassified. Root cause was narrower than it looked: dropdown_options already had the 4 canonical values and ClientFormDialog already had the field fully wired (state/defaults/hydration) β€” the only gap was the missing form_fields config row.

  4. βœ… FIXED (Flutter #358) β€” Deals β€” commission fields can independently disagree. Web computes commission = amount Γ— commission_percentage / 100 with no separate commission input, so the three numbers can never conflict. Flutter exposes all three as independent editable fields with no linkage β€” the same deal can show a different commission depending which app last touched it. Real financial-data-integrity risk.

  5. βœ… FIXED (Flutter #358) β€” Deals β€” form-driven stage edits skip every side effect. Changing stage via web's edit dialog cascades: request status flip, client lifecycle update, WhatsApp notify, auto-reminder. Flutter's dedicated stage-action-bar does a partial subset (closed_at + request status only, no client update / notify / reminder) β€” and critically, editing stage via the ordinary form Save path (not the action bar) fires zero side effects on Flutter. A stage edited through the form silently drops every downstream effect web guarantees. Note: the fix closes the gap for the subset the action bar already handled (closed_at + linked-request status); full parity on client-lifecycle update / WhatsApp notify / auto-reminder-on-stage-change is a larger follow-up, not replicated here.

  6. βœ… FIXED (web #1078) β€” Valuations β€” web's staff workflow screen blank-renders 6 of 11 legitimate workflow_stage values. ValuationWorkflow.tsx casts workflow_stage directly to a UI-stage key with no synonym folding; Flutter's valuationStoredStageToUiKey map correctly normalizes document_review/inspection_scheduled/inspection/valuation/report_drafted/delivery onto the 7-key stepper. A row with any of those 6 values (this is most of the canonical vocabulary β€” see Phase 2 audit) renders with no stage selected on web's staff screen. This is the mirror image of the "workflow_stage UI collapses to 7 keys" pattern already known to be symmetric between the two apps β€” it turns out only Flutter actually implements the fold correctly.

  7. βœ… FIXED (web #1078) β€” Valuations β€” "approved before delivered" gate is inconsistently strict on web itself. The stepper-navigation gate accepts approval_status ∈ {approved, review} as sufficient to reach the delivered stage, while the actual "Deliver" button separately enforces strict === 'approved'. Flutter's single gate is strict everywhere. Not a cross-app drift so much as an internal web inconsistency worth closing to strict-everywhere.

  8. βœ… FIXED (Flutter #358) β€” Properties β€” numeric fields have zero client-side validation on Flutter. al_misaha (size), al_sier/al_sawm (price/bid), al_adwar (floor count), irtidad_distance (setback) all lack any validator: in the Flutter form. Web enforces positive() + explicit upper bounds on size/price via zod, with a code comment specifically noting the size requirement was a prior data-quality regression fix ("allowing null lets garbage listings into the catalog") β€” Flutter has silently reintroduced exactly that regression. None of these columns have a DB CHECK, so bad values genuinely persist.

  9. OPEN β€” needs a product decision. Properties β€” duplicate-listing detection uses entirely different logic per app. Web triggers on blur of the PACI number field, matching by PACI number, offering "proceed" or "deactivate existing." Flutter only checks at submit time, matching by governorate+area+block+plot (parcel) and separately by address+file-number β€” neither the trigger timing nor the match key nor the remediation options overlap. Two independently-designed duplicate guards protecting the same table. Not fixed: unifying these is a design decision (which match key/trigger point/remediation UX becomes canonical), not a one-line bug fix.

  10. βœ… FIXED (web #1078) β€” Requests β€” web's own staff dialog has a dead al_gharad (purpose) control. The field is fully typed, hydrated, and resubmitted by RequestFormData, but no component in RequestBasicInfo/RequestPropertyPreferences/RequestBudgetTimeline actually renders a control for it β€” new requests filed via the staff CRM dialog always submit purpose: null. This is a web bug, independent of Flutter (which fully implements the field). Only the separate public intake form still exposes it.

  11. ❌ FALSE POSITIVE β€” no fix needed. Requests β€” client auto-create is asymmetric. The original claim: web's request form auto-creates a new clients row when a typed name/phone doesn't resolve to an existing client, while Flutter only resolves against existing ones. This didn't survive verification: Flutter's resolveClientIdFromNameAndPhone calls the find_or_create_unified_person and find_or_create_client_for_party RPCs β€” confirmed against the live function bodies that the latter does INSERT INTO clients (step 3 of its logic) when no existing/linked/phone-matched row is found. Flutter creates a client the same as web; the two just use different mechanisms (web: a direct clients insert with no unified_persons linkage; Flutter: routes through the party registry with phone-based dedup). That mechanism difference might itself be worth a look some day (web's path doesn't touch unified_persons at all), but it is not the "Flutter drops new contacts" bug originally reported.

Secondary findings (drift, lower severity or already-mitigated elsewhere) ​

EntityFindingVerdict
ClientsWeb's primary-phone validation is a loose /^[0-9+\-\s]{0,20}$/; Flutter enforces strict Kuwait-mobile E.164 normalization. Same clients.phone column ends up in two different formats depending on which app wrote it.DRIFT
ClientsWeb requires phone on every edit (not just create) due to a shared form-fields config bug, contradicting its own migration's stated intent; Flutter correctly scopes the requirement to create-only.DRIFT (web bug)
Clientscivil_id and editable assigned_agent exist only on web (Flutter has no field / read-only display).FLUTTER-MISSING (documented mobile scope)
RequestsBudget/size fields: web enforces positive() + upper caps; Flutter's nonNegativeNumber allows 0 and has no upper bound.DRIFT
RequestsProperty-type β†’ area gating (web prunes invalid area/category combos) has no Flutter equivalent β€” mobile can save geographically-invalid combinations web would block.DRIFT
RequestsArea Groups picker and the "Buyer Journey" guided wizard are web-only.WEB-ONLY (documented mobile-scope reduction)
DealsStage-transition completeness gate (required fields per stage before advancing) exists on web, not on Flutter β€” mobile permits advancing with incomplete data web would block.DRIFT
Dealscommission_percentage defaults to '1' on web, null on Flutter for an untouched new deal.DRIFT
DealsConfirmed FIXED: Flutter's stage dropdown now offers all 7 canonical stages including open (a historical bug, verified resolved). Confirmed SAFE: a web-created off_market deal loads on Flutter without crashing (synthetic fallback menu item), even though Flutter's own create form correctly excludes it as a choice.β€” (verification only)
ValuationsPer-stage required-field validation, comparable-sales attachment, applicant-type conditional acknowledgement fields, and auto-invoice-on-delivery are all web-only, consistent with the mobile screen's documented scope.WEB-ONLY (documented)
FinanceWeb only soft-caps payment amount via an HTML max attribute (not enforced in JS); Flutter strictly blocks amounts exceeding balance due.DRIFT (Flutter stricter, web allows overpayment)
FinanceFor mixed per-line tax rates, web's saved invoice/quotation total is computed from a single blended header rate (ignoring per-item tax_rate); Flutter's per-line computation is internally consistent and more correct.DRIFT
FinanceQuotation β†’ invoice conversion exists only on web; no Flutter equivalent found.WEB-ONLY (not confirmed intentional β€” worth asking)
FinanceFlutter's invoice-status filter chips omit partially_paid as a filterable option, though such invoices still display correctly in the unfiltered list (not hidden).DRIFT (minor, cosmetic)
FinancePossible shared DB-level issue (not an app-parity bug): the original payment-status trigger migration writes Arabic literals ('Ω…Ψ―ΩΩˆΨΉΨ©'/'Ω…Ψ―ΩΩˆΨΉΨ© Ψ¬Ψ²Ψ¦ΩŠΨ§Ω‹') while a later CHECK constraint requires English values. Not confirmed whether the trigger itself was ever updated β€” flagged for separate DB-level verification, out of scope for this audit.UNVERIFIED β€” needs its own check
PropertiesOwner/party assignment: web's schema marks it optional; Flutter's _submit() imperatively blocks create without one β€” Flutter is stricter than web for the same DB write path.DRIFT (inconsistent strictness)
PropertiesTags editor and photo upload are both deliberately deferred off Flutter's create/edit wizard (explicit code comments), reachable via separate mobile screens instead.FLUTTER-MISSING (documented mobile-scope reduction)

Verified matches (no action needed) ​

  • Properties: building composition / floor-unit breakdown (shares the v3 contract, PR #351) β€” full parity.
  • Requests: price_groups multi-select, al_hala status (no conditional sections on either side).
  • Clients: lifecycle_stage has no transition guard on either side; option taxonomies identical (per Phase 2).
  • Deals: checklist/doc auto-tracking (checklist_data.auto) β€” parity at the detail-screen level.
  • Interactions: entity-type/entity-id polymorphic linkage, channel/direction/outcome visibility, summary requiredness.
  • Finance: payment-method conditional fields (neither side has any), salary_deduction correctly excluded from manual payment dialogs on both sides, invoice-status auto-transition on payment (both apps correctly delegate to the same DB trigger, no app-level logic to drift).

Next step ​

9 of 11 priority findings fixed (web #1078, Flutter #358); #11 was a false positive (corrected above); #9 needs a product decision before it can be built. The secondary-findings table above also has several open items worth a decision: requests' budget/size bounds and property-type→area gating, deals' stage-completeness gate, finance's quotation→invoice conversion (web-only — worth confirming intentional) and overpayment validation, and the possible shared DB-level Arabic/English trigger mismatch on invoices.status (flagged as unverified, needs its own check independent of this audit).

Phase 4 (output/PDF parity) is the next phase of the program.

Aldilaijan & Khobara Real Estate Platform