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 class | CAD Vendor |
|---|---|
GenericXmlParser | Any vendor with standard XML export |
TriTechParser | TriTech Inform CAD |
CentralSquareParser | CentralSquare CAD |
NewWorldAegisParser | NewWorld 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_xmlis always stored oncfs_incidents— allows re-parsing if field mappings change later- Files are never deleted — moved to
processed_pathwith 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