Skip to main content

Field Force & Sales CRM (FFM)

Module code FFM · Release R3 · Packaging Add-on (priced per tracked user) · Requirements FR-FFM-*, BR-FFM-*, RULE-FFM-* in the PRD and BRD · Related Attendance, Claims, Mobile, field visit to travel claim

1. Overview and scope​

FFM serves SMEs with people in the field (pharma and medical reps, FMCG distribution, education, insurance, real estate). It joins where the person was (tracking, visits, kilometres) with what they did (accounts, activities, pipeline, collections) and what it cost (travel → mileage claims).

In scope: tracking modes (point-to-point and continuous) only between punch-in and punch-out, with explicit consent; location ingest; daily travel computation with outlier filtering and optional snap-to-road; geo-verified visits at accounts; beat (route) plans; accounts and contacts (shared org_account master), client associates (e.g. doctors); activities (visit, call, message, mail) with follow-ups; a configurable pipeline with stage templates; won value and payment collection; manager live map with battery level; device approval (IAM); travel verification with revised km and reason; reports.

Out of scope: orders, quotes, product catalogue and inventory (later), territory optimisation.

2. Learned from the reference product​

Source: Spryple analysis, §11, §16 and §21.7.

  • [Observed] GPS monitoring on a map (today or past date) with battery %, a tracking log (start, stay duration, travel duration, km), and two modes: Point-to-Point and GPS Monitoring (§11; §21.7).
  • [Observed] A hard-coded, demo-centric pipeline: Leads, Call for Demo, Demo Pending, Demo Completed, Future Leads, plus a separate client status (Interested, Follow-up, Won, Lost, Not Interested); lead age colours green 0–7, amber 8–14, red 15+ days (§11; §21.7).
  • [Observed] Visit location shows matched, not matched or not available; if the client has no location, the first visit's location is used (§21.7).
  • [Observed] Travel approvals have an editable "KM Revised" with a selected-records summary; a new phone needs manager device approval (§11; §21.7).
  • [Observed] Client associates (type Doctor, qualification MBBS, specialization Cardiology) and client payments (method, collected, pending) (§21.7).
  • [Inferred] No documented consent, work-hours limit or retention for tracking; km appear GPS-derived without reconciliation (§11).

3. Our design decisions​

IDDecision [Proposed]Reason
D-FFM-01Tracking runs only between punch-in and punch-out, after recorded consent, with an on-screen indicator; raw points kept 90 days, then daily aggregates only.DPDP and trust.
D-FFM-02Pipelines are tenant data (stages with order, probability, won/lost flags) with templates: "demo-led SaaS", "pharma detailing", "distributor onboarding".Removes the hard-coded pipeline.
D-FFM-03Visit check-in is verified against the account location with a policy action: BLOCK, FLAG or ALLOW.Goes beyond the reference indicator.
D-FFM-04Daily distance is computed server-side from filtered points (accuracy, speed, jitter), optionally snapped to roads; managers may revise with a mandatory reason.Defensible mileage.
D-FFM-05Daily travel feeds CLM as a mileage claim; the separate travel-approval path is retired.One reimbursement pipeline.
D-FFM-06Location ingest goes to the ingest process role and monthly-partitioned tables.Write-heavy path; extraction candidate.

4. Domain model​

5. State machines​

5.1 Daily travel​

5.2 Opportunity​

6. Key business logic​

6.1 GPS distance with outlier filtering​

dailyDistance(points sorted by recorded_at, policy):
# 1. Drop unusable points
pts = points where accuracy_m <= policy.max_accuracy_m and not is_mock
# 2. Drop impossible jumps (speed filter)
kept = [pts[0]]
for p in pts[1:]:
d = haversine(kept.last, p) ; t = seconds(p.recorded_at - kept.last.recorded_at)
if t <= 0: continue
if d / t > policy.max_speed_mps: continue # e.g. 45 m/s, about 160 km/h
kept.append(p)
# 3. Collapse stationary jitter (stays)
segments = [] ; anchor = kept[0]
for p in kept[1:]:
if haversine(anchor, p) < policy.jitter_radius_m: continue # e.g. 30 m, a stay
segments.append(anchor, p) ; anchor = p
# 4. Sum distance
if policy.snap_to_road and segments non-empty:
km = roadsApi.distance(segments) / 1000 # provider interface, batched
else:
km = sum(haversine(a, b) for a, b in segments) / 1000 * policy.straight_line_factor
# 5. Fill gaps: a gap longer than policy.max_gap_minutes between kept points is
# bridged by straight-line distance and flagged GAP for the reviewer
return round(km, 1), flags

haversine(a, b) = 2 * R * asin(sqrt(sin^2(dlat/2) + cos(lat_a) * cos(lat_b) * sin^2(dlng/2)))
with R = 6371000 m

Point-to-point mode uses only punch-in, visit check-ins and punch-out as points, with snap-to-road between them.

6.2 Visit verification​

checkIn(employee, account, point):
if account has no location: set account location from this check-in (as observed), result NOT_AVAILABLE
d = haversine(point, account.location)
result = MATCHED if d <= policy.visit_radius_m + point.accuracy_m else NOT_MATCHED
apply policy.visit_mismatch_action (BLOCK, FLAG, ALLOW)

6.3 Lead age​

age_days = today - stage_entered_on; colour bands come from pipeline.age_bands (default 0–7 green, 8–14 amber, 15+ red, as observed).

7. Configuration​

SettingTypeDefaultDescription
tracking.modeenumPOINT_TO_POINTPOINT_TO_POINT or CONTINUOUS
tracking.interval_secondsinteger120Sampling interval in continuous mode
tracking.consent_requiredbooleantrueAlways true in production
tracking.raw_retention_daysinteger90Raw point retention
distance.max_accuracy_minteger50Points worse than this are dropped
distance.max_speed_mpsnumber45Speed filter
distance.jitter_radius_minteger30Stationary collapse radius
distance.max_gap_minutesinteger15Gap flag threshold
distance.snap_to_roadbooleantrueUse the roads provider
distance.straight_line_factornumber1.2Correction when not snapping
visit.radius_minteger150Match radius
visit.mismatch_actionenumFLAGBLOCK, FLAG, ALLOW
visit.photo_requiredbooleanfalseImage-only activities option [Observed]
pipeline.age_bandsarray of integer[7, 14]Lead-age colour thresholds
travel.min_visits_for_claiminteger0Minimum visits filter [Observed]
travel.auto_submitbooleantrueSubmit at punch-out
accounts.create_from_webbooleantrueReference toggle [Observed]

8. Screens by persona​

PersonaWebMobile
Field executiveAccounts, pipeline board, activities, travel historyPunch with photo, today's beat, check-in and out, add account and activity, payments, follow-ups, travel summary
Sales managerLive map with battery and last ping, tracking log, pipeline board with reassignment, beat planning, travel verification with revised km, device approvalsTeam location, travel verify
Sales adminPipelines and stages, activity types, associate types, qualifications, specializations, visit policy—
Org ownerField dashboard: active on field, visits, km, won valueTiles

9. Events​

EventConsumers
ffm.session.started, ffm.session.endedMOB (tracking on and off), RPT
ffm.visit.checked_inATT (optional auto-punch at first visit), NTF (manager, if flagged)
ffm.visit.completedRPT
ffm.opportunity.stage_changedRPT, NTF
ffm.opportunity.wonNTF, RPT
ffm.payment.collectedNTF (finance), RPT
ffm.travel.day_closedNTF (employee to review)
ffm.travel.verifiedCLM (create mileage claim)

Consumed: att.punch.recorded (start or stop a session), ext.separation.approved (stop tracking after LWD), iam.device.approved, clm.claim.paid (travel status).

10. API surface​

ResourceMain operations
ingest.spyrl.app/v1/ffm/pointsPOST batch of points (ingest role)
/v1/ffm/sessionsGET by employee and date, live team positions
/v1/ffm/visitsPOST check-in, POST /:id/check-out, GET
/v1/ffm/beat-plansCRUD
/v1/ffm/pipelines, /v1/ffm/opportunitiesCRUD, POST /:id/stage
/v1/ffm/activitiesCRUD
/v1/ffm/associates, /v1/ffm/paymentsCRUD
/v1/ffm/daily-travelGET, POST /:id/submit, POST /:id/verify with revised km

Accounts and contacts are /v1/org/accounts and /v1/org/contacts. Detailed in API endpoints.

11. Permissions​

PermissionScopesDefault roles
ffm.tracking.readTEAM, LOCATIONsales_manager (sensitive, audited)
ffm.visit.createSELFfield_executive
ffm.opportunity.manageSELF, TEAMfield_executive, sales_manager
ffm.opportunity.reassignTEAMsales_manager
ffm.travel.verifyTEAMsales_manager
ffm.pipeline.manageORGsales_admin
ffm.beat.manageTEAMsales_manager
ffm.payment.recordSELFfield_executive
ffm.report.exportTEAM, ORGsales_manager, org_owner

12. Reports​

ReportContentsReference
Employee activityPunch in and out, visits, type, km[Observed] §11
Tracking logStart, stays, travel durations, km, end[Observed] §11
Employee summaryFirst login, last logout, hours, new visits, new accounts, km, activities[Observed] §11
Pipeline by stage and ageCount, value, ageing[Observed] §11, configurable [Proposed]
Visit compliancePlanned vs visited, matched vs not matched[Proposed]
CollectionsCollected vs pending by account[Observed] §21.7
Travel and mileageComputed vs revised km, claims[Observed] §21.7

13. Release and priority​

CapabilityReleaseMoSCoW
Consented tracking, sessions, live map with batteryR3Must
Daily distance with outlier filtering; travel verification; mileage claimR3Must
Accounts, activities, configurable pipeline, follow-upsR3Must
Geo-verified visitsR3Must
Beat plansR3Should
Associates and paymentsR3Should
Snap-to-road providerR3Should
Orders and catalogueR4+Won't (this release)

14. Open questions​

  1. Google Maps Platform or Mappls for roads and geocoding (cost per tracked user per month)? Needs a pricing spike.
  2. What sampling interval balances battery and accuracy on low-end Android under Doze? Field test needed.
  3. Do pharma customers need MCI-style associate data validation or call-frequency norms per doctor class?