Skip to main content

CFS Integration (CAD File Polling)

Each PSAP can have one or more cfs_configurations rows — one per CAD system or data feed. The Symfony Scheduler registers a CfsPollerTask for each active configuration at boot.

cfs_configurations (one or more per PSAP)
|
v (Symfony Scheduler — one task per active config)
CfsPollerTask(config_id)
├── Glob inbox_path for files matching file_pattern
├── SHA-256 hash each file → skip if hash exists in cfs_imports (dedup)
├── CfsParserService::parse(file, config)
| └── Instantiates config.parser_class + applies config.field_mappings
├── Upsert cfs_incidents (unique on cfs_configuration_id + external_cfs_id)
├── Write cfs_imports row (status, hash, record count)
├── Update cfs_configurations.last_polled_at / last_poll_status
└── Move file → processed_path/<utc-timestamp>-<filename> (never delete)

PSAP "New Incident" UI
└── GET /api/v1/cfs-incidents?status=active (scoped to agency's configs)
└── Dispatcher selects CFS → POST /api/v1/incidents { cfs_incident_id, agency_ids }

Built-in Parsers

Parser classCAD Vendor
GenericXmlParserAny vendor with standard XML export
TriTechParserTriTech Inform CAD
CentralSquareParserCentralSquare CAD
NewWorldAegisParserNewWorld Aegis

Adding a New Vendor

Implement CfsParserInterface:

interface CfsParserInterface
{
/** Returns an array of ParsedCfsRecord objects. */
public function parse(string $xmlContent, CfsConfiguration $config): array;
}

Set parser_class to the FQCN in the cfs_configurations row. No core changes required.

Key Design Decisions

  • raw_xml is always stored on cfs_incidents — allows re-parsing if field mappings change later
  • Files are never deleted — moved to processed_path with a UTC timestamp prefix
  • Deduplication is by SHA-256 file hash — same file delivered twice is a no-op
  • Upsert on (cfs_configuration_id, external_cfs_id) — CFS incidents are idempotent