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
| ID | Decision [Proposed] | Reason |
|---|---|---|
| D-FFM-01 | Tracking 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-02 | Pipelines 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-03 | Visit check-in is verified against the account location with a policy action: BLOCK, FLAG or ALLOW. | Goes beyond the reference indicator. |
| D-FFM-04 | Daily 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-05 | Daily travel feeds CLM as a mileage claim; the separate travel-approval path is retired. | One reimbursement pipeline. |
| D-FFM-06 | Location 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
| Setting | Type | Default | Description |
|---|---|---|---|
tracking.mode | enum | POINT_TO_POINT | POINT_TO_POINT or CONTINUOUS |
tracking.interval_seconds | integer | 120 | Sampling interval in continuous mode |
tracking.consent_required | boolean | true | Always true in production |
tracking.raw_retention_days | integer | 90 | Raw point retention |
distance.max_accuracy_m | integer | 50 | Points worse than this are dropped |
distance.max_speed_mps | number | 45 | Speed filter |
distance.jitter_radius_m | integer | 30 | Stationary collapse radius |
distance.max_gap_minutes | integer | 15 | Gap flag threshold |
distance.snap_to_road | boolean | true | Use the roads provider |
distance.straight_line_factor | number | 1.2 | Correction when not snapping |
visit.radius_m | integer | 150 | Match radius |
visit.mismatch_action | enum | FLAG | BLOCK, FLAG, ALLOW |
visit.photo_required | boolean | false | Image-only activities option [Observed] |
pipeline.age_bands | array of integer | [7, 14] | Lead-age colour thresholds |
travel.min_visits_for_claim | integer | 0 | Minimum visits filter [Observed] |
travel.auto_submit | boolean | true | Submit at punch-out |
accounts.create_from_web | boolean | true | Reference toggle [Observed] |
8. Screens by persona
| Persona | Web | Mobile |
|---|---|---|
| Field executive | Accounts, pipeline board, activities, travel history | Punch with photo, today's beat, check-in and out, add account and activity, payments, follow-ups, travel summary |
| Sales manager | Live map with battery and last ping, tracking log, pipeline board with reassignment, beat planning, travel verification with revised km, device approvals | Team location, travel verify |
| Sales admin | Pipelines and stages, activity types, associate types, qualifications, specializations, visit policy | — |
| Org owner | Field dashboard: active on field, visits, km, won value | Tiles |
9. Events
| Event | Consumers |
|---|---|
ffm.session.started, ffm.session.ended | MOB (tracking on and off), RPT |
ffm.visit.checked_in | ATT (optional auto-punch at first visit), NTF (manager, if flagged) |
ffm.visit.completed | RPT |
ffm.opportunity.stage_changed | RPT, NTF |
ffm.opportunity.won | NTF, RPT |
ffm.payment.collected | NTF (finance), RPT |
ffm.travel.day_closed | NTF (employee to review) |
ffm.travel.verified | CLM (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
| Resource | Main operations |
|---|---|
ingest.spyrl.app/v1/ffm/points | POST batch of points (ingest role) |
/v1/ffm/sessions | GET by employee and date, live team positions |
/v1/ffm/visits | POST check-in, POST /:id/check-out, GET |
/v1/ffm/beat-plans | CRUD |
/v1/ffm/pipelines, /v1/ffm/opportunities | CRUD, POST /:id/stage |
/v1/ffm/activities | CRUD |
/v1/ffm/associates, /v1/ffm/payments | CRUD |
/v1/ffm/daily-travel | GET, 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
| Permission | Scopes | Default roles |
|---|---|---|
ffm.tracking.read | TEAM, LOCATION | sales_manager (sensitive, audited) |
ffm.visit.create | SELF | field_executive |
ffm.opportunity.manage | SELF, TEAM | field_executive, sales_manager |
ffm.opportunity.reassign | TEAM | sales_manager |
ffm.travel.verify | TEAM | sales_manager |
ffm.pipeline.manage | ORG | sales_admin |
ffm.beat.manage | TEAM | sales_manager |
ffm.payment.record | SELF | field_executive |
ffm.report.export | TEAM, ORG | sales_manager, org_owner |
12. Reports
| Report | Contents | Reference |
|---|---|---|
| Employee activity | Punch in and out, visits, type, km | [Observed] §11 |
| Tracking log | Start, stays, travel durations, km, end | [Observed] §11 |
| Employee summary | First login, last logout, hours, new visits, new accounts, km, activities | [Observed] §11 |
| Pipeline by stage and age | Count, value, ageing | [Observed] §11, configurable [Proposed] |
| Visit compliance | Planned vs visited, matched vs not matched | [Proposed] |
| Collections | Collected vs pending by account | [Observed] §21.7 |
| Travel and mileage | Computed vs revised km, claims | [Observed] §21.7 |
13. Release and priority
| Capability | Release | MoSCoW |
|---|---|---|
| Consented tracking, sessions, live map with battery | R3 | Must |
| Daily distance with outlier filtering; travel verification; mileage claim | R3 | Must |
| Accounts, activities, configurable pipeline, follow-ups | R3 | Must |
| Geo-verified visits | R3 | Must |
| Beat plans | R3 | Should |
| Associates and payments | R3 | Should |
| Snap-to-road provider | R3 | Should |
| Orders and catalogue | R4+ | Won't (this release) |
14. Open questions
- Google Maps Platform or Mappls for roads and geocoding (cost per tracked user per month)? Needs a pricing spike.
- What sampling interval balances battery and accuracy on low-end Android under Doze? Field test needed.
- Do pharma customers need MCI-style associate data validation or call-frequency norms per doctor class?